~nesv/govern

301ff934f925bf629653e58e0548ac19376ecdf0 — Nick Saika 1 year, 3 months ago 669793b
runners/pkg: Refactor

I had an moment of inspiration: instead of having to implement functions
specific to each, supported Linux distribution and/or operating system
(e.g. *BSD), the commands for each supported OS or distribution could be
written down in a JSON file, embedded within the runner, and loaded at
startup.
2 files changed, 209 insertions(+), 22 deletions(-)

A runners/pkg/distributions.json
M runners/pkg/main.go
A runners/pkg/distributions.json => runners/pkg/distributions.json +26 -0
@@ 0,0 1,26 @@
{
  "Arch Linux": {
    "separator": "=",
    "installed": [
      "pacman",
      "-Sq",
      "--needed",
      "--noconfirm"
    ],
    "latest": [
      "pacman",
      "-Syq",
      "--needed",
      "--noconfirm"
    ],
    "absent": [
      "pacman",
      "-R",
      "--noconfirm"
    ],
    "query": [
      "pacman",
      "-Q"
    ]
  }
}

M runners/pkg/main.go => runners/pkg/main.go +183 -22
@@ 2,8 2,12 @@
package main

import (
	_ "embed"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"

	"git.sr.ht/~nesv/govern/runner"
)


@@ 16,8 20,43 @@ func main() {
}

func run() error {
	distros, err := loadDistributions()
	if err != nil {
		return fmt.Errorf("load config: %w", err)
	}

	r := runner.Runner{
		DefaultState:stateInstalled,
		DefaultState: "installed",
		States: map[string]runner.StateFunc{
			"installed": distros.install,
			"latest":    distros.ensureLatest,
			"absent":    distros.remove,
		},
		RequiredFacts: []string{
			"os/distribution",
		},
	}

	return r.Exec()
}

//go:embed distributions.json
var distributionsJSON []byte

func loadDistributions() (distributions, error) {
	dd := make(distributions)
	if err := json.Unmarshal(distributionsJSON, &dd); err != nil {
		return nil, err
	}

	return dd, nil
}

type distributions map[string]distroConfig

func (d distributions) install(r *runner.Runner, name string, attrs map[string]string) error {
	if d == nil {
		return errors.New("not initialized")
	}

	distro, err := r.Fact("os/distribution")


@@ 25,33 64,155 @@ func run() error {
		return fmt.Errorf("get fact: %w", err)
	}

	handlers, ok := distroHandlers[distro]
	if !ok {
	config, found := d[distro]
	if !found {
		return fmt.Errorf("distribution not supported: %s", distro)
	}

	r.States=handlers
	return r.Exec()
	return config.install(r, name, attrs)
}

const (
	stateInstalled = "installed"
	stateLatest = "latest"
	stateRemoved = "removed"
)
func (d distributions) ensureLatest(r *runner.Runner, name string, attrs map[string]string) error {
	if d == nil {
		return errors.New("not initialized")
	}

	distro, err := r.Fact("os/distribution")
	if err != nil {
		return fmt.Errorf("get fact: %w", err)
	}

	config, found := d[distro]
	if !found {
		return fmt.Errorf("distribution not supported: %s", distro)
	}

	return config.ensureLatest(r, name, attrs)
}

func (d distributions) remove(r *runner.Runner, name string, attrs map[string]string) error {
	if d == nil {
		return errors.New("not initialized")
	}

	distro, err := r.Fact("os/distribution")
	if err != nil {
		return fmt.Errorf("get fact: %w", err)
	}

	config, found := d[distro]
	if !found {
		return fmt.Errorf("distribution not supported: %s", distro)
	}

	return config.remove(r, name, attrs)
}

var distroHandlers = map[string]map[string]runner.StateFunc{
	"Arch Linux": {
		stateInstalled: archInstalled,
		stateLatest: archLatest,
		stateRemoved: archRemoved,
	},
type distroConfig struct {
	Separator string   `json:"separator"`
	Installed []string `json:"installed"`
	Latest    []string `json:"latest"`
	Absent    []string `json:"absent"`
	Query     []string `json:"query"`
}

var commands = map[string]map[string][]string{
	"Arch Linux": {
		"installed": {"pacman", "-Sq", "--needed", "--noconfirm"},
		"latest":    {"pacman", "-Syq", "--needed", "--noconfirm"},
		"absent":    {"pacman", "-R", "--noconfirm"},
	},
func (d distroConfig) install(r *runner.Runner, name string, attrs map[string]string) error {
	pkgName := name
	if v := attrs["version"]; v != "" {
		pkgName = fmt.Sprintf("%s%s%s", pkgName, d.Separator, v)
	}

	if r.Pretend() {
		installed, err := d.pkgInstalled(pkgName)
		if err != nil {
			return fmt.Errorf("get installed version: %w", err)
		}

		if installed {
			return runner.OK()
		}

		fmt.Println("would be installed")
		return nil
	}

	return runner.OK()
}

func (d distroConfig) pkgInstalled(name string) (bool, error) {
	version, err := d.pkgversion(name)
	if err != nil {
		return false, err
	}

	return version != "", nil
}

func (d distroConfig) pkgversion(name string) (string, error) {
	out, err := runner.Command(d.Query...)
	if err != nil {
		return "", err
	}

	fields := strings.Fields(out)
	if n := len(fields); n < 2 {
		return "", fmt.Errorf("expected 2 fields, got %d", n)
	}

	return fields[1], nil
}

func (d distroConfig) ensureLatest(r *runner.Runner, name string, attrs map[string]string) error {
	version := attrs["version"]

	if r.Pretend() {
		installedVersion, err := d.pkgversion(name)
		if err != nil {
			return err
		}

		if installedVersion == "" {
			fmt.Println("will be installed")
			return nil
		}

		if installedVersion != version {
			fmt.Println("will be upgraded")
			return nil
		}

		return runner.OK()
	}

	if version != "" {
		name = fmt.Sprintf("%s%s%s", name, d.Separator, version)
	}

	if _, err := runner.Command(d.Latest...); err != nil {
		return err
	}

	return runner.OK()
}

func (d distroConfig) remove(r *runner.Runner, name string, attrs map[string]string) error {
	if r.Pretend() {
		version, err := d.pkgversion(name)
		if err != nil {
			return err
		}

		if version == "" {
			return runner.OK()
		}

		fmt.Println("will be removed")
		return nil
	}

	if _, err := runner.Command(d.Absent...); err != nil {
		return err
	}

	return runner.OK()
}