From feccdf634366ca726154bc1810cf9f3fd0090033 Mon Sep 17 00:00:00 2001 From: Link Dupont Date: Tue, 18 Jan 2022 15:10:39 -0500 Subject: [PATCH] feat: add repository management to PackageManager Expand the PackageManager interface to include repository management: adding repository content, removing repository content, enabling existing repositories, and disabling existing repositories. --- README.md | 44 +++++++++++++++++++++++++----- package_manager.go | 8 ++++++ package_manager_apt.go | 52 +++++++++++++++++++++++++++++++++-- package_manager_dnf.go | 26 ++++++++++++++++-- package_manager_yum.go | 30 +++++++++++++++++++-- server.go | 52 +++++++++++++++++++++++++++++++---- util.go | 61 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 256 insertions(+), 17 deletions(-) create mode 100644 util.go diff --git a/README.md b/README.md index 27abd87..3937436 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 { @@ -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 diff --git a/package_manager.go b/package_manager.go index 28d7a6d..2859efd 100644 --- a/package_manager.go +++ b/package_manager.go @@ -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) { diff --git a/package_manager_apt.go b/package_manager_apt.go index 71a3396..7de873c 100644 --- a/package_manager_apt.go +++ b/package_manager_apt.go @@ -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) { @@ -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 } diff --git a/package_manager_dnf.go b/package_manager_dnf.go index c0e227b..e05bd9d 100644 --- a/package_manager_dnf.go +++ b/package_manager_dnf.go @@ -1,7 +1,10 @@ package main import ( + "io/ioutil" + "os" "os/exec" + "path/filepath" ) type PackageManagerDnf struct{} @@ -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 } diff --git a/package_manager_yum.go b/package_manager_yum.go index c07e880..09f59be 100644 --- a/package_manager_yum.go +++ b/package_manager_yum.go @@ -1,7 +1,10 @@ package main import ( + "io/ioutil" + "os" "os/exec" + "path/filepath" ) type PackageManagerYum struct{} @@ -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 } diff --git a/server.go b/server.go index 1b29534..e6880d8 100644 --- a/server.go +++ b/server.go @@ -16,6 +16,7 @@ import ( type Message struct { Command string `json:"command"` Name string `json:"name"` + Content []byte `json:"content"` } type Output struct { @@ -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) @@ -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) @@ -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 diff --git a/util.go b/util.go new file mode 100644 index 0000000..d65b5c0 --- /dev/null +++ b/util.go @@ -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 +}