pkg.go

v0.3.0
Doc Versions Source
1
package steps
2
3
import (
4
	"strings"
5
6
	"go.bigb.es/auxilia/culpa"
7
	"go.bigb.es/auxilia/legatus"
8
)
9
10
// PkgState is the desired state of a package.
11
type PkgState string
12
13
const (
14
	// PkgPresent requires the package to be installed.
15
	PkgPresent PkgState = "present"
16
	// PkgAbsent requires the package to be removed.
17
	PkgAbsent PkgState = "absent"
18
)
19
20
// PkgManager identifies the host package manager. PC3: this battery carries
21
// apt/dnf/pacman opinions. An unknown manager is an error, never a silent default.
22
type PkgManager string
23
24
const (
25
	// Apt is Debian/Ubuntu apt-get.
26
	Apt PkgManager = "apt"
27
	// Dnf is Fedora/RHEL dnf.
28
	Dnf PkgManager = "dnf"
29
	// Pacman is Arch Linux pacman.
30
	Pacman PkgManager = "pacman"
31
)
32
33
// Package manages an OS package. Check queries whether it is installed; Apply
34
// installs or removes it to converge State.
35
type Package struct {
36
	Pkg     string
37
	State   PkgState
38
	Manager PkgManager
39
}
40
41
// Name returns the step name.
42
func (p Package) Name() string { return "package:" + p.Pkg }
43
44
// Check reports a change is needed when the package's installed state differs
45
// from the desired State.
46
func (p Package) Check(fc *legatus.FlowCtx) (bool, error) {
47
	installed, err := p.isInstalled(fc)
48
	if err != nil {
49
		return false, err
50
	}
51
	switch p.State {
52
	case PkgPresent:
53
		return !installed, nil
54
	case PkgAbsent:
55
		return installed, nil
56
	default:
57
		return false, culpa.WithCode(culpa.Errorf("package %s: unknown state %q", p.Pkg, p.State), "PKG_STATE")
58
	}
59
}
60
61
// Apply installs or removes the package.
62
func (p Package) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
63
	cmd, err := p.applyCmd()
64
	if err != nil {
65
		return legatus.Outcome{}, err
66
	}
67
	res, err := fc.Conn().Exec(fc.Ctx(), cmd)
68
	if err != nil {
69
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "package %s %s", p.State, p.Pkg), "PKG_APPLY")
70
	}
71
	if res.ExitCode != 0 {
72
		return legatus.Outcome{}, culpa.WithCode(
73
			culpa.Errorf("package %s %s: exit %d: %s", p.State, p.Pkg, res.ExitCode, strings.TrimSpace(string(res.Combined()))),
74
			"PKG_APPLY")
75
	}
76
	return legatus.Outcome{Changed: true, Message: string(p.State) + " " + p.Pkg}, nil
77
}
78
79
// isInstalled queries the manager for the package's presence.
80
func (p Package) isInstalled(fc *legatus.FlowCtx) (bool, error) {
81
	switch p.Manager {
82
	case Apt:
83
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
84
			Path: "dpkg-query",
85
			Args: []string{"-W", "-f=${Status}", p.Pkg},
86
		})
87
		if err != nil {
88
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
89
		}
90
		if res.ExitCode != 0 {
91
			return false, nil
92
		}
93
		return strings.Contains(string(res.Stdout), "install ok installed"), nil
94
	case Dnf:
95
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
96
			Path: "rpm",
97
			Args: []string{"-q", p.Pkg},
98
		})
99
		if err != nil {
100
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
101
		}
102
		return res.ExitCode == 0, nil
103
	case Pacman:
104
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
105
			Path: "pacman",
106
			Args: []string{"-Q", p.Pkg},
107
		})
108
		if err != nil {
109
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
110
		}
111
		return res.ExitCode == 0, nil
112
	default:
113
		return false, culpa.WithCode(culpa.Errorf("package %s: unknown manager %q", p.Pkg, p.Manager), "PKG_MANAGER")
114
	}
115
}
116
117
// applyCmd builds the install/remove command for the manager and state.
118
func (p Package) applyCmd() (legatus.Cmd, error) {
119
	var path string
120
	switch p.Manager {
121
	case Apt:
122
		path = "apt-get"
123
	case Dnf:
124
		path = "dnf"
125
	case Pacman:
126
		verb := "-S" // present
127
		if p.State == PkgAbsent {
128
			verb = "-R"
129
		}
130
		return legatus.Cmd{Path: "pacman", Args: []string{verb, "--noconfirm", p.Pkg}, Sudo: true}, nil
131
	default:
132
		return legatus.Cmd{}, culpa.WithCode(culpa.Errorf("package %s: unknown manager %q", p.Pkg, p.Manager), "PKG_MANAGER")
133
	}
134
135
	var verb string
136
	switch p.State {
137
	case PkgPresent:
138
		verb = "install"
139
	case PkgAbsent:
140
		verb = "remove"
141
	default:
142
		return legatus.Cmd{}, culpa.WithCode(culpa.Errorf("package %s: unknown state %q", p.Pkg, p.State), "PKG_STATE")
143
	}
144
145
	return legatus.Cmd{
146
		Path: path,
147
		Args: []string{"-y", verb, p.Pkg},
148
		Env:  map[string]string{"DEBIAN_FRONTEND": "noninteractive"},
149
		Sudo: true,
150
	}, nil
151
}
152

Source Files