Skip to content

Commit

Permalink
feat: add repository management to PackageManager
Browse files Browse the repository at this point in the history
Expand the PackageManager interface to include repository management:
adding repository content, removing repository content, enabling
existing repositories, and disabling existing repositories.
  • Loading branch information
subpop committed Jan 19, 2022
1 parent b7f054d commit feccdf6
Show file tree
Hide file tree
Showing 7 changed files with 256 additions and 17 deletions.
44 changes: 38 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# yggdrasil-worker-package-manager

`yggdrasil-worker-package-manager` is a simple package manager yggd worker. It
knows how to install and remove packages, and does rudamentary detection of the
host its running on to guess the package manager to use. It only installs
packages that match one of the provided `allow-pattern` regular expressions.
knows how to install and remove packages, add, remove, enable and disable
repositories, and does rudamentary detection of the host its running on to guess
the package manager to use. It only installs packages that match one of the
provided `allow-pattern` regular expressions.

# Installation

Expand Down Expand Up @@ -32,14 +33,17 @@ directive. It expects to receive messages with the following JSON schema:
{
"type": "object",
"properties": {
"command": { "enum": ["install", "remove"] },
"name": { "type": "string" }
"command": { "enum": ["install", "remove", "enable-repo", "disable-repo", "add-repo", "remove-repo"] },
"name": { "type": "string" },
"content": { "type": "string" }
},
"required": ["command", "name" ]
}
```

For example, to tell `yggd-package-manager-worker` to install "vim", send it:
## Examples

### Install `vim`

```json
{
Expand All @@ -48,6 +52,34 @@ For example, to tell `yggd-package-manager-worker` to install "vim", send it:
}
```

### Enable "updates-testing" repository

```json
{
"command": "enable-repo",
"name": "updates-testing"
}
```

### Add custom repository on a dnf or yum client

```json
{
"command": "add-repo",
"name": "my-custom-repo",
"content": "[my-custom-repo]\nbaseurl=http://servername/path/to/repo\nenabled=1"
}
```

### Add custom repository on an apt client

```json
{
"command": "add-repo",
"name": "deb http://servername path component"
}
```

# Permitting operations

Before an operation on a package is permitted, the value of the `name` field is
Expand Down
8 changes: 8 additions & 0 deletions package_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ type Uninstaller interface {
Uninstall(name string) (stdout, stderr []byte, code int, err error)
}

type RepositoryManager interface {
AddRepo(name string, content []byte) (stdout, stderr []byte, code int, err error)
RemoveRepo(name string) (stdout, stderr []byte, code int, err error)
EnableRepo(name string) (stdout, stderr []byte, code int, err error)
DisableRepo(name string) (stdout, stderr []byte, code int, err error)
}

type PackageManager interface {
Installer
Uninstaller
RepositoryManager
}

func run(cmd *exec.Cmd) (stdout, stderr []byte, code int, err error) {
Expand Down
52 changes: 50 additions & 2 deletions package_manager_apt.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package main

import (
"fmt"
"os"
"os/exec"
"path/filepath"
)

const sourcesFile = "/etc/apt/sources.list.d/yggdrasil-package-manager.list"

type PackageManagerApt struct{}

func (p *PackageManagerApt) Install(name string) (stdout, stderr []byte, code int, err error) {
Expand All @@ -14,8 +19,51 @@ func (p *PackageManagerApt) Uninstall(name string) (stdout, stderr []byte, code
return p.run("remove", name)
}

func (p *PackageManagerApt) run(command, name string) (stdout, stderr []byte, code int, err error) {
cmd := exec.Command("/usr/bin/apt", command, "--assumeyes", name)
func (p *PackageManagerApt) AddRepo(sourceLine string, _ []byte) (stdout, stderr []byte, code int, err error) {
return p.EnableRepo(sourceLine)
}

func (p *PackageManagerApt) RemoveRepo(sourceLine string) (stdout, stderr []byte, code int, err error) {
return p.DisableRepo(sourceLine)
}

func (p *PackageManagerApt) EnableRepo(sourceLine string) (stdout, stderr []byte, code int, err error) {
if err := os.MkdirAll(filepath.Base(sourcesFile), 0755); err != nil {
return nil, nil, -1, fmt.Errorf("cannot create sources list directory: %w", err)
}

if err := writeLines(sourcesFile, []string{sourceLine + "\n"}, false); err != nil {
return nil, nil, 1, fmt.Errorf("cannot write to sources list file: %w", err)
}

return nil, nil, 0, nil
}

func (p *PackageManagerApt) DisableRepo(sourceLine string) (stdout, stderr []byte, code int, err error) {
lines, err := readLines(sourcesFile)
if err != nil {
return nil, nil, -1, fmt.Errorf("cannot read sources list file: %w", err)
}

for i, l := range lines {
if l == sourceLine {
copy(lines[i:], lines[i+1:])
lines = lines[:len(lines)-1]
break
}
}

if err := writeLines(sourcesFile, lines, true); err != nil {
return nil, nil, -1, fmt.Errorf("cannot write sources list file: %w", err)
}

return nil, nil, 0, nil
}

func (p *PackageManagerApt) run(command string, args ...string) (stdout, stderr []byte, code int, err error) {
cmdargs := []string{"--assume-yes", command}
cmdargs = append(cmdargs, args...)
cmd := exec.Command("/usr/bin/apt-get", cmdargs...)
stdout, stderr, code, err = run(cmd)
return
}
26 changes: 24 additions & 2 deletions package_manager_dnf.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package main

import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
)

type PackageManagerDnf struct{}
Expand All @@ -14,8 +17,27 @@ func (p *PackageManagerDnf) Uninstall(name string) (stdout, stderr []byte, code
return p.run("remove", name)
}

func (p *PackageManagerDnf) run(command, name string) (stdout, stderr []byte, code int, err error) {
cmd := exec.Command("/usr/bin/dnf", command, "--assumeyes", name)
func (p *PackageManagerDnf) AddRepo(name string, content []byte) (stdout, stderr []byte, code int, err error) {
return nil, nil, -1, ioutil.WriteFile(filepath.Join("/etc/yum.repos.d/", canonicalizeRepoName(name, ".repo")), content, 0644)
}

func (p *PackageManagerDnf) RemoveRepo(name string) (stdout, stderr []byte, code int, err error) {
return nil, nil, -1, os.Remove(filepath.Join("/etc/yum.repos.d/", canonicalizeRepoName(name, ".repo")))
}

func (p *PackageManagerDnf) EnableRepo(name string) (stdout, stderr []byte, code int, err error) {
return p.run("config-manager", "--enable", name)
}

func (p *PackageManagerDnf) DisableRepo(name string) (stdout, stderr []byte, code int, err error) {
return p.run("config-manager", "--disable", name)
}

func (p *PackageManagerDnf) run(command string, args ...string) (stdout, stderr []byte, code int, err error) {
cmdargs := []string{"--assumeyes", command}
cmdargs = append(cmdargs, args...)

cmd := exec.Command("/usr/bin/dnf", cmdargs...)
stdout, stderr, code, err = run(cmd)
return
}
30 changes: 28 additions & 2 deletions package_manager_yum.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package main

import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
)

type PackageManagerYum struct{}
Expand All @@ -14,8 +17,31 @@ func (p *PackageManagerYum) Uninstall(name string) (stdout, stderr []byte, code
return p.run("remove", name)
}

func (p *PackageManagerYum) run(command, name string) (stdout, stderr []byte, code int, err error) {
cmd := exec.Command("/usr/bin/yum", command, "--assumeyes", name)
func (p *PackageManagerYum) AddRepo(name string, content []byte) (stdout, stderr []byte, code int, err error) {
return nil, nil, -1, ioutil.WriteFile(filepath.Join("/etc/yum.repos.d/", canonicalizeRepoName(name, ".repo")), content, 0644)
}

func (p *PackageManagerYum) RemoveRepo(name string) (stdout, stderr []byte, code int, err error) {
return nil, nil, -1, os.Remove(filepath.Join("/etc/yum.repos.d/", canonicalizeRepoName(name, ".repo")))
}

func (p *PackageManagerYum) EnableRepo(name string) (stdout, stderr []byte, code int, err error) {
cmd := exec.Command("/usr/bin/yum-config-manager", "--assumeyes", "--enable", name)
stdout, stderr, code, err = run(cmd)
return
}

func (p *PackageManagerYum) DisableRepo(name string) (stdout, stderr []byte, code int, err error) {
cmd := exec.Command("/usr/bin/yum-config-manager", "--assumeyes", "--disable", name)
stdout, stderr, code, err = run(cmd)
return
}

func (p *PackageManagerYum) run(command string, args ...string) (stdout, stderr []byte, code int, err error) {
cmdargs := []string{"--assumeyes", command}
cmdargs = append(cmdargs, args...)

cmd := exec.Command("/usr/bin/yum", cmdargs...)
stdout, stderr, code, err = run(cmd)
return
}
52 changes: 47 additions & 5 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
type Message struct {
Command string `json:"command"`
Name string `json:"name"`
Content []byte `json:"content"`
}

type Output struct {
Expand Down Expand Up @@ -43,16 +44,16 @@ func (s *Server) Send(ctx context.Context, d *protocol.Data) (*protocol.Receipt,
}
log.Debugf("received command: %v", m)

if !s.packageAllowed(m.Name) {
log.Errorf("cannot install %v: does not match an allow pattern", m.Name)
return
}

var stdout, stderr []byte
var code int
var err error
switch m.Command {
case "install":
if !s.packageAllowed(m.Name) {
log.Errorf("cannot install %v: does not match an allow pattern", m.Name)
return
}

stdout, stderr, code, err = s.pm.Install(m.Name)
if err != nil {
log.Errorf("cannot install package: %v", err)
Expand All @@ -63,6 +64,11 @@ func (s *Server) Send(ctx context.Context, d *protocol.Data) (*protocol.Receipt,
}
log.Infof("installed package: %v", m.Name)
case "remove":
if !s.packageAllowed(m.Name) {
log.Errorf("cannot remove %v: does not match an allow pattern", m.Name)
return
}

stdout, stderr, code, err = s.pm.Uninstall(m.Name)
if err != nil {
log.Errorf("cannot remove package: %v", err)
Expand All @@ -72,6 +78,42 @@ func (s *Server) Send(ctx context.Context, d *protocol.Data) (*protocol.Receipt,
return
}
log.Infof("removed package: %v", m.Name)
case "enable-repo":
stdout, stderr, code, err = s.pm.EnableRepo(m.Name)
if err != nil {
log.Errorf("cannot enable repository: %v", err)
log.Debugf("program exited with code %v", code)
log.Debugf("program stderr:\n%v", string(stderr))
log.Debugf("program stdout:\n%v", string(stdout))
return
}
case "disable-repo":
stdout, stderr, code, err = s.pm.DisableRepo(m.Name)
if err != nil {
log.Errorf("cannot disable repository: %v", err)
log.Debugf("program exited with code %v", code)
log.Debugf("program stderr:\n%v", string(stderr))
log.Debugf("program stdout:\n%v", string(stdout))
return
}
case "add-repo":
stdout, stderr, code, err = s.pm.AddRepo(m.Name, m.Content)
if err != nil {
log.Errorf("cannot add repository: %v", err)
log.Debugf("program exited with code %v", code)
log.Debugf("program stderr:\n%v", string(stderr))
log.Debugf("program stdout:\n%v", string(stdout))
return
}
case "remove-repo":
stdout, stderr, code, err = s.pm.RemoveRepo(m.Name)
if err != nil {
log.Errorf("cannot remove repository: %v", err)
log.Debugf("program exited with code %v", code)
log.Debugf("program stderr:\n%v", string(stderr))
log.Debugf("program stdout:\n%v", string(stdout))
return
}
default:
log.Errorf("cannot perform command: %v", m.Command)
return
Expand Down
61 changes: 61 additions & 0 deletions util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import (
"bufio"
"fmt"
"io/ioutil"
"os"
"strings"
)

// canonicalizeRepoName converts the string filename into a suitable filename.
// It replaces all spaces with underscores and appends the given suffix.
func canonicalizeRepoName(filename, suffix string) string {
return strings.TrimSuffix(strings.ReplaceAll(filename, " ", "_"), suffix) + suffix
}

// readLines reads each line in file into a slice.
func readLines(name string) ([]string, error) {
file, err := os.Open(name)
if err != nil {
return nil, fmt.Errorf("cannot open file for reading: %w", err)
}
defer file.Close()

var lines []string
scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
lines = append(lines, scanner.Text())
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("cannot read file: %w", err)
}

return lines, nil
}

// writeLines writes lines to file name. If truncate is true, the file is
// truncated before writing.
func writeLines(name string, lines []string, truncate bool) error {
var data []byte

for _, line := range lines {
data = append(data, line+"\n"...)
}

if truncate {
return ioutil.WriteFile(name, data, 0644)
} else {
file, err := os.OpenFile(name, os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return fmt.Errorf("cannot open file for appending: %w", err)
}
defer file.Close()

if _, err := file.Write(data); err != nil {
return fmt.Errorf("cannot write to file: %w", err)
}
}
return nil
}

0 comments on commit feccdf6

Please sign in to comment.