~nesv/govern

00a0d9d67dc8942be78a3c3c440c232f3ee5decd — Nick Saika 1 year, 9 months ago 4568044
runners/file: New runner
M .gitignore => .gitignore +2 -1
@@ 1,4 1,5 @@
bin/**
libexec/**

# Created by https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode
# Edit at https://www.toptal.com/developers/gitignore?templates=go,emacs,vim,vscode


@@ 104,4 105,4 @@ tags
!.vscode/extensions.json
*.code-workspace

# End of https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode
\ No newline at end of file
# End of https://www.toptal.com/developers/gitignore/api/go,emacs,vim,vscode

M GNUmakefile => GNUmakefile +20 -9
@@ 1,4 1,11 @@
TARGETS	:= bin/govern
NAME	:= govern
PACKAGE	:= git.sr.ht/~nesv/govern
DESTDIR	:= ${CURDIR}
RUNNERS	:= file pkg service
TARGETS	:= ${DESTDIR}/bin/${NAME} \
	   ${DESTDIR}/libexec/${NAME}/runner/file \
	   ${DESTDIR}/libexec/${NAME}/runner/pkg \
	   ${DESTDIR}/libexec/${NAME}/runner/service
SOURCES	:= $(wildcard internal/agent/*.go) \
	$(wildcard internal/exec/*.go) \
	$(wildcard internal/facts/*.go) \


@@ 8,7 15,7 @@ SOURCES	:= $(wildcard internal/agent/*.go) \
	$(wildcard internal/server/*.go) \
	$(wildcard internal/state/*.go)

GO	:= $(shell which go 2>/dev/null)
GO	?= $(shell which go 2>/dev/null)
ifeq (${GO},"")
	$(error Cannot find go in your $$PATH)
endif


@@ 20,15 27,19 @@ endif

all: ${TARGETS}

bin/govern: $(wildcard *.go) ${SOURCES}
${DESTDIR}/bin/${NAME}: $(wildcard *.go) ${SOURCES}
	${GO} build ${GC_FLAGS} -o $@

.PHONY: runners
runners: bin/runner/pkg

bin/runner/pkg: $(wildcard runners/pkg/*.go) $(wildcard runner/*.go)
	${GO} build ${GC_FLAGS} -o $@ ./runners/pkg
${DESTDIR}/libexec/${NAME}/runner/%: $(wildcard runners/%/*.go) $(wildcard runner/*.go)
	${GO} build ${GC_FLAGS} -o $@ ${PACKAGE}/runners/$*

.PHONY: clean
clean:
	rm -rf bin
	-rm ${TARGETS}

.PHONY: check
check:
	go vet ./...
ifneq ($(shell comamnd -v staticcheck 2>/dev/null),)
	staticcheck ./...
endif

M cmd/hclast/main.go => cmd/hclast/main.go +1 -14
@@ 4,13 4,11 @@ import (
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"os"
	"strings"

	"github.com/hashicorp/hcl"
	"github.com/hashicorp/hcl/hcl/ast"
	"github.com/hashicorp/hcl/hcl/printer"
	hclstrconv "github.com/hashicorp/hcl/hcl/strconv"
)



@@ 30,7 28,7 @@ func main() {
	defer f.Close()

	lr := io.LimitReader(f, 1<<20)
	p, err := ioutil.ReadAll(lr)
	p, err := io.ReadAll(lr)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)


@@ 104,14 102,3 @@ func fmtResourceName(i *ast.ObjectItem) (string, error) {
	}
	return strings.Join(elem, ":"), nil
}

func walkFunc(node ast.Node) (ast.Node, bool) {
	if node == nil {
		return nil, false
	}

	fmt.Printf("=== %#v\n", node)
	printer.Fprint(os.Stdout, node)
	fmt.Println()
	return node, true
}

M example.config.hcl => example.config.hcl +1 -2
@@ 1,7 1,6 @@
facts "./facts" {}

runners "./bin/runner" {}
runners "./runners" {}
runners "./libexec/govern/runner" {}

states "./example/state" {}


M go.mod => go.mod +12 -2
@@ 1,14 1,24 @@
module git.sr.ht/~nesv/govern

go 1.16
go 1.19

require (
	github.com/go-mangos/mangos v1.2.0
	github.com/hashicorp/hcl v1.0.0
	github.com/hashicorp/hcl/v2 v2.9.1
	github.com/nesv/cmndr v1.1.0
	github.com/pkg/errors v0.9.1
	github.com/rs/xid v0.0.0-20170604230408-02dd45c33376
	github.com/stretchr/testify v1.7.0 // indirect
	github.com/zclconf/go-cty v1.8.0
	k8s.io/apimachinery v0.20.4
)

require (
	github.com/agext/levenshtein v1.2.1 // indirect
	github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
	github.com/google/go-cmp v0.5.2 // indirect
	github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
	github.com/spf13/pflag v1.0.5 // indirect
	github.com/stretchr/testify v1.7.0 // indirect
	golang.org/x/text v0.3.5 // indirect
)

M go.sum => go.sum +2 -1
@@ 6,7 6,6 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8=
github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0=
github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk=
github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw=
github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo=


@@ 69,6 68,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.9.1 h1:eOy4gREY0/ZQHNItlfuEZqtcQbXIxzojlP301hDpnac=
github.com/hashicorp/hcl/v2 v2.9.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=

A runner/command.go => runner/command.go +30 -0
@@ 0,0 1,30 @@
package runner

import (
	"errors"
	"os/exec"
)

// Command is a convenience function that runs the given command,
// and if successful will return the command's output (if any).
// If the command does not exist successfully,
// err will be non-nil,
// and output will contain the contents of STDERR.
func Command(args ...string) (output string, err error) {
	var cmd *exec.Cmd
	if len(args) > 2 {
		cmd = exec.Command(args[0], args[1:]...)
	} else {
		cmd = exec.Command(args[0])
	}

	out, err := cmd.Output()
	var exit *exec.ExitError
	if errors.As(err, &exit) {
		return string(exit.Stderr), err
	} else if err != nil {
		return "", err
	}

	return string(out), nil
}

A runner/helper.go => runner/helper.go +10 -0
@@ 0,0 1,10 @@
package runner

import "fmt"

// OK is a convenience function that prints "ok" to STDOUT,
// and returns a nil error.
func OK() error {
	fmt.Println("ok")
	return nil
}

M runner/runner.go => runner/runner.go +28 -11
@@ 23,14 23,23 @@ type Runner struct {
	// DefaultState is the default value for the "state" attribute.
	// When a state file-defined resource does not have its "state"
	// attribute specified, DefaultState will be used.
	//
	// The caller is still required to register a handler function with
	// r.HandleState.
	DefaultState string

	// Run is the function Runner will execute,
	// after parsing command-line arguments,
	// and environment variables.
	Run func(runner *Runner, resourceName string, attributes map[string]string) error
	// States maps a resource state name to a handler function that will
	// ensure that desired state.
	//
	// Note that govern does not make any impositions on what a "valid"
	// state name is.
	States map[string]StateFunc
}

// StateFunc defines a function type that is responsible for ensuring a
// resource's state.
type StateFunc func(r *Runner, resourceName string, attributes map[string]string) error

// Exec executes the runner.
//
// Exec will ensure that there are at least three (3) arguments provided on the


@@ 52,17 61,15 @@ func (r *Runner) Exec() error {
	return r.ExecArgs(name, args...)
}

//
// ExecArgs executes the runner with the resource name and any arguments.
func (r *Runner) ExecArgs(name string, args ...string) error {
	if r.Run == nil {
		return errors.New("nil run function")
	if len(r.States) == 0{
		return errors.New("no state handlers registered")
	}

	// Parse the command-line args into attributes.
	// They are expected to be "key=value" pairs.
	attrs := map[string]string{
		"state": r.DefaultState,
	}
	attrs := make(map[string]string)
	for _, v := range args {
		s := strings.SplitN(v, "=", 2)
		if len(s) < 2 {


@@ 71,7 78,17 @@ func (r *Runner) ExecArgs(name string, args ...string) error {
		attrs[s[0]] = s[1]
	}

	return r.Run(r, name, attrs)
	state, ok := attrs["state"]
	if !ok {
		state=r.DefaultState
	}

	handler, ok  := r.States[state]
	if !ok {
		return fmt.Errorf("no handler registered for state: %s", state)
	}

	return handler(r, name, attrs)
}

// Fact retrieves the fact with the given name,

A runners/file.lua => runners/file.lua +1 -0
@@ 0,0 1,1 @@
#!/usr/bin/env lua

M runners/file/main.go => runners/file/main.go +42 -20
@@ 19,15 19,18 @@ import (
	"strconv"
	"strings"

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

	"github.com/pkg/errors"

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

func main() {
	r := runner.Runner{
		DefaultState: "present",
		Run:          run,
		States: map[string]runner.StateFunc{
			"present": present,
			"absent":  absent,
		},
	}
	if err := r.Exec(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)


@@ 35,18 38,32 @@ func main() {
	}
}

func run(r *runner.Runner, name string, attrs map[string]string) error {
	// Make sure the path name is absolute.
func absent(r *runner.Runner, name string, attrs map[string]string) error {
	name, err := cleanPath(name)
	if err != nil {
		return err
	}

	if err := os.Remove(name); err != nil {
		return fmt.Errorf("remove: %w", err)
	}

	return nil
}

func cleanPath(name string) (string, error) {
	name = filepath.Clean(name)
	if !filepath.IsAbs(name) {
		return errors.New("path must be absolute")
		return "", errors.New("path name must be absolute")
	}
	return name, nil
}

	state := attrs["state"]
	switch state {
	case "absent", "present":
	default:
		return fmt.Errorf("unknown state: %s", state)
func present(r *runner.Runner, name string, attrs map[string]string) error {
	// Make sure the path name is absolute.
	name, err := cleanPath(name)
	if err != nil {
		return err
	}

	// Does the file exist?


@@ 61,22 78,16 @@ func run(r *runner.Runner, name string, attrs map[string]string) error {
	// Assume the file exists.
	//
	// For directories, the directory runner should be used instead.
	if info.IsDir() && state == "present" {
	if info != nil && info.IsDir() {
		return errors.New("path refers to a directory")
	}

	// See if we need to delete the file, or update its contents.
	if state == "absent" {
		// The file does not exist, and the caller has indicated the
		// file should not exist.
		return errors.Wrap(os.Remove(name), "remove")
	}

	// Check to see if the existing file has the same
	// checksum digest as the one specified in the "checksum"
	// attribute.
	// If the checksums match, we do not have to update its
	// contents.
	var fetched bool
	if checksum := attrs["checksum"]; checksum != "" {
		checksumsMatch, err := compareChecksum(name, checksum)
		if err != nil {


@@ 91,6 102,7 @@ func run(r *runner.Runner, name string, attrs map[string]string) error {
			if err != nil {
				return errors.Wrap(err, "fetch")
			}
			fetched = true

			// Make sure the checksum of the new file matches the
			// checksum given in the resource attribute.


@@ 107,6 119,16 @@ func run(r *runner.Runner, name string, attrs map[string]string) error {
		}
	}

	if !fetched {
		tmppath, err := fetch(attrs["source"], filepath.Dir(name))
		if err != nil {
			return errors.Wrap(err, "fetch")
		}
		if err := os.Rename(tmppath, name); err != nil {
			return errors.Wrap(err, "replace existing file")
		}
	}

	// Set the mode.
	if err := chmod(name, attrs["mode"]); err != nil {
		return errors.Wrap(err, "chmod")


@@ 292,7 314,7 @@ func gethash(checksum string) (h hash.Hash, digest []byte, err error) {

	parts := strings.SplitN(checksum, ":", 2)
	if len(parts) < 2 {
		return nil, nil, errors.New("invalid checksum")
		return nil, nil, errors.New("invalid checksum: missing \":\" separator")
	}

	switch algo := parts[0]; algo {

A runners/pkg.lua => runners/pkg.lua +23 -0
@@ 0,0 1,23 @@
#!/usr/bin/env lua

--[[ local govern_bin = os.getenv("GOVERN")
assert(govern_bin ~= nil, "GOVERN not set in environment")

local govern_facts_path = os.getenv("GOVERN_FACTS_PATH")
assert(govern_facts_path ~= nil, "GOVERN_FACTS_PATH not set in environment") ]]

local Commands = {}
Commands["Arch Linux"] = {
	installed = "pacman -Sq --needed --noconfirm",
	latest = "pacman -Syq --needed --noconfirm",
	absent = "pacman -R --noconfirm",
	_query = "pacman -Q",
}

local CommandToExecute = { Commands["Arch Linux"]._query }
for k, v in ipairs(arg) do
	CommandToExecute[#CommandToExecute+1] = tostring(v)
end
local cmd = table.concat(CommandToExecute, " ")
print(":: " .. cmd)
os.execute(cmd)

A runners/pkg/arch_linux.go => runners/pkg/arch_linux.go +119 -0
@@ 0,0 1,119 @@
package main

import (
	"fmt"
	"strings"

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

func archInstalled(r *runner.Runner, name string, attrs map[string]string) error {
	packageName := name
	if v := attrs["version"]; v != "" {
		packageName = fmt.Sprintf("%s%s%s", packageName, "=", v)
	}

	// Are we running in "pretend" (a.k.a. "no-op mode")?
	if r.Pretend() {
		installed, err := archPackageInstalled(packageName)
		if err != nil {
			return fmt.Errorf("get installed version: %w", err)
		}

		if installed {
			return runner.OK()
		}

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

	// Run the command!
	if _, err := runner.Command("sudo", "pacman", "-Sq", "--needed", "--noconfirm", packageName); err != nil {
		return err
	}

	return runner.OK()
}

func archPackageInstalled(pkgname string) (bool, error) {
	version, err := archInstalledVersion(pkgname)
	if err != nil {
		return false, err
	}

	return version != "", nil
}

func archInstalledVersion(pkgname string) (string, error) {
	output, err := runner.Command("pacman", "-Q", pkgname)
	if err != nil && strings.Contains(output, "was not found") {
		return "", nil
	} else if err != nil {
		return "", fmt.Errorf("query pacman: %w", err)
	}

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

	return fields[1], nil
}

func archLatest(r *runner.Runner, name string, attrs map[string]string) error {
	version := attrs["version"]
	if r.Pretend() {
		installedVersion, err := archInstalledVersion(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", name, version)
	}

	if _, err := runner.Command("sudo", "pacman", "-Syq", "--needed", "--noconfirm", name); err != nil {
		return err
	}

	return runner.OK()
}

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

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

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

	output, err := runner.Command("sudo", "pacman", "-R", "--noconfirm", name)
	if err != nil && strings.Contains(output, "target not found") {
		return runner.OK()
	} else if err != nil {
		return err
	}

	return runner.OK()
}

M runners/pkg/main.go => runners/pkg/main.go +24 -78
@@ 2,86 2,50 @@
package main

import (
	"errors"
	"fmt"
	"os"
	"os/exec"

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

func main() {
	r := runner.Runner{
		DefaultState: "installed",
		Run:          run,
	}
	if err := r.Exec(); err != nil {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

func run(r *runner.Runner, name string, attrs map[string]string) error {
	distro, err := r.Fact("os/distribution")
	if err != nil {
		return fmt.Errorf("get fact %q: %w", "os/distribution", err)
	}

	packageName := name
	if v := attrs["version"]; v != "" {
		packageName = fmt.Sprintf("%s%s%s", packageName, packageVersionSeparator[distro], v)
func run() error {
	r := runner.Runner{
		DefaultState:stateInstalled,
	}

	// Make sure the "state" attribute is one we understand.
	state := attrs["state"]
	switch state {
	case "installed", "latest", "absent":
	default:
		return fmt.Errorf("unknown state: %s", state)
	distro, err := r.Fact("os/distribution")
	if err != nil {
		return fmt.Errorf("get fact: %w", err)
	}

	// Are we running in "pretend" (a.k.a. "no-op mode")?
	if r.Pretend() {
		installed, err := packageInstalled(distro, packageName)
		if err != nil {
			return fmt.Errorf("get installed version: %w", err)
		}

		switch state {
		case "installed", "latest":
			if !installed {
				fmt.Println("would be installed/upgraded")
				return nil
			}
			fmt.Println("ok")

		case "absent":
			if !installed {
				fmt.Println("ok")
				return nil
			}
			fmt.Println("would be uninstalled")
		}
		return nil
	handlers, ok := distroHandlers[distro]
	if !ok {
		return fmt.Errorf("distribution not supported: %s", distro)
	}

	// Run the command!
	var (
		args  = append(commands[distro][state], packageName)
		cmd   = exec.Command("sudo", args...)
		exerr *exec.ExitError
	)
	out, err := cmd.CombinedOutput()
	if errors.As(err, &exerr) {
		return fmt.Errorf("%s", out)
	} else if err != nil {
		return err
	}
	return nil
	r.States=handlers
	return r.Exec()
}

var packageVersionSeparator = map[string]string{
	"Arch Linux": "=",
const (
	stateInstalled = "installed"
	stateLatest = "latest"
	stateRemoved = "removed"
)

var distroHandlers = map[string]map[string]runner.StateFunc{
	"Arch Linux": {
		stateInstalled: archInstalled,
		stateLatest: archLatest,
		stateRemoved: archRemoved,
	},
}

var commands = map[string]map[string][]string{


@@ 91,21 55,3 @@ var commands = map[string]map[string][]string{
		"absent":    {"pacman", "-R", "--noconfirm"},
	},
}

// packageInstalled indicates if the package name is installed.
func packageInstalled(distro, pkgname string) (bool, error) {
	cmd := exec.Command(queryPackage[distro][0], queryPackage[distro][1:]...)
	out, err := cmd.Output()

	var exiterr *exec.ExitError
	if errors.As(err, &exiterr) {
		return false, errors.New(string(exiterr.Stderr))
	} else if err != nil {
		return false, err
	}
	return len(out) > 0, nil
}

var queryPackage = map[string][]string{
	"Arch Linux": {"pacman", "-Q"},
}

D runners/service => runners/service +0 -4
@@ 1,4 0,0 @@
#!/bin/sh
set -eu

exit 0

A runners/service/internal/systemd/systemd_linux.go => runners/service/internal/systemd/systemd_linux.go +171 -0
@@ 0,0 1,171 @@
package systemd

import (
	"errors"
	"fmt"
	"os/exec"

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

// Start activates a systemd unit.
// If attrs["enabled"] is a "true",
// Start will also enable the unit.
func Start(r *runner.Runner, name string, attrs map[string]string) error {
	active, err := isActive(name)
	if err != nil {
		return fmt.Errorf("is active: %w", err)
	}

	enabled, err := isEnabled(name)
	if err != nil {
		return fmt.Errorf("is enabled: %w", err)
	}

	var wantEnabled bool
	if v := attrs["enabled"]; v == "true" {
		wantEnabled = true
	}

	if active && enabled && wantEnabled {
		return runner.OK()
	}

	if r.Pretend() {
		enable := enabled && wantEnabled
		if !active && enable {
			fmt.Println("will be activated and enabled")
			return nil
		}
		if active && !enable {
			fmt.Println("will be enabled")
			return nil
		}
		if !active {
			fmt.Println("will be activated")
			return nil
		}
		return runner.OK()
	}

	if active && !enabled && wantEnabled {
		return Enable(r, name, attrs)
	}

	// Activate the unit.
	cmd := []string{"sudo", "systemctl"}
	if wantEnabled {
		cmd = append(cmd, "enable", "--now", name)
	} else {
		cmd = append(cmd, "start", name)
	}

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

	return runner.OK()
}

func isActive(name string) (bool, error) {
	output, err := runner.Command("systemctl", "is-active", name)
	var exit *exec.ExitError
	if errors.As(err, &exit) {
		switch code := exit.ExitCode(); code {
		case 1:
			// program is dead and /var/run pid file exists
			// unit not failed (used by is-failed)
			return true, nil
		case 2:
			// unused
			panic("systemctl is-enabled exit code=2")
		case 3:
			// program is not running
			return false, nil
		case 4:
			// no such unit
			return false, nil
		}
		panic(fmt.Sprintf("systemctl is-active: code=%d", exit.ExitCode()))
	} else if err != nil {
		return false, err
	}

	switch output {
	case "active":
		return true, nil
	case "inactive":
		return false, nil
	}

	panic("how did we get here?")
}

func isEnabled(name string) (bool, error) {
	output, err := runner.Command("systemctl", "is-enabled", name)
	var exit *exec.ExitError
	if errors.As(err, &exit) {
		switch code := exit.ExitCode(); code {
		case 1:
			// program is dead and /var/run pid file exists
			return false, nil
		case 2:
			// unused
			panic("systemctl is-enabled code=2")
		case 3:
			// program is not running
			panic("systemctl is-enabled code=3")
		case 4:
			// no such unit
			return false, nil
		}
		panic(fmt.Sprintf("systemctl is-active: code=%d", exit.ExitCode()))
	} else if err != nil {
		return false, err
	}

	if output == "enabled" {
		return true, nil
	}

	return false, nil
}

// Enable enables a systemd unit.
func Enable(r *runner.Runner, name string, attrs map[string]string) error {
	enabled, err := isEnabled(name)
	if err != nil {
		return fmt.Errorf("is enabled: %w", err)
	}

	if enabled {
		return runner.OK()
	}

	if _, err := runner.Command("sudo", "systemctl", "enable", name); err != nil {
		return err
	}
	return runner.OK()
}

// Disable disables a systemd unit.
func Disable(r *runner.Runner, name string, attrs map[string]string) error {
	enabled, err := isEnabled(name)
	if err != nil {
		return fmt.Errorf("is enabled: %w", err)
	}

	if !enabled {
		return runner.OK()
	}

	if _, err := runner.Command("sudo", "systemctl", "disable", name); err != nil {
		return err
	}

	return runner.OK()
}

func Stop(r *runner.Runner, name string, attrs map[string]string) error {
	return errors.New("not implemented")
}

A runners/service/main.go => runners/service/main.go +60 -0
@@ 0,0 1,60 @@
package main

import (
	"errors"
	"fmt"
	"os"
	"os/exec"

	"git.sr.ht/~nesv/govern/runner"
	"git.sr.ht/~nesv/govern/runners/service/internal/systemd"
)

func main() {
	if err := run(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

func run() error {
	r := runner.Runner{
		DefaultState: "started",
	}

	handlers, err := detectInitSystem()
	if err != nil {
		return err
	}
	r.States = handlers

	return r.Exec()
}

func detectInitSystem() (map[string]runner.StateFunc, error) {
	for name, handlers := range inithandlers {
		if _, err := exec.LookPath(name); err != nil {
			return nil, fmt.Errorf("lookup: %w", err)
		}

		return handlers, nil
	}

	return nil, errors.New("no supported init system found")
}

var inithandlers = map[string]map[string]runner.StateFunc{
	"systemctl": {
		"running":   systemd.Start,
		"stopped":   systemd.Stop,
		"restarted": noop,
		"reloaded":  noop,
		"enabled":   systemd.Enable,
		"disabled":  systemd.Disable,
		"masked":    noop,
	},
}

func noop(r *runner.Runner, name string, attrs map[string]string) error {
	return nil
}