pkg.go

v0.6.1
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
	// Sudo controls privilege-wrapping of the install/remove; the zero value
40
	// (SudoDefault) sudo-wraps, preserving the previous always-sudo behavior.
41
	Sudo SudoPolicy
42
}
43
44
// Name returns the step name.
45
func (p Package) Name() string { return "package:" + p.Pkg }
46
47
// Check reports a change is needed when the package's installed state differs
48
// from the desired State.
49
func (p Package) Check(fc *legatus.FlowCtx) (bool, error) {
50
	installed, err := p.isInstalled(fc)
51
	if err != nil {
52
		return false, err
53
	}
54
	switch p.State {
55
	case PkgPresent:
56
		return !installed, nil
57
	case PkgAbsent:
58
		return installed, nil
59
	default:
60
		return false, culpa.WithCode(culpa.Errorf("package %s: unknown state %q", p.Pkg, p.State), "PKG_STATE")
61
	}
62
}
63
64
// Apply installs or removes the package.
65
func (p Package) Apply(fc *legatus.FlowCtx) (legatus.Outcome, error) {
66
	mgr, err := p.effectiveManager(fc)
67
	if err != nil {
68
		return legatus.Outcome{}, err
69
	}
70
	cmd, err := p.applyCmd(mgr, p.Sudo.resolve(fc.Host(), true))
71
	if err != nil {
72
		return legatus.Outcome{}, err
73
	}
74
	res, err := fc.Conn().Exec(fc.Ctx(), cmd)
75
	if err != nil {
76
		return legatus.Outcome{}, culpa.WithCode(culpa.Wrapf(err, "package %s %s", p.State, p.Pkg), "PKG_APPLY")
77
	}
78
	if res.ExitCode != 0 {
79
		return legatus.Outcome{}, culpa.WithCode(
80
			culpa.Errorf("package %s %s: exit %d: %s", p.State, p.Pkg, res.ExitCode, strings.TrimSpace(string(res.Combined()))),
81
			"PKG_APPLY")
82
	}
83
	return legatus.Outcome{Changed: true, Message: string(p.State) + " " + p.Pkg}, nil
84
}
85
86
// effectiveManager returns the configured Manager, or auto-detects it when
87
// Manager is empty by probing the host for pacman/apt-get/dnf in that order. An
88
// unidentifiable host is an error (PC3: never a silent default).
89
func (p Package) effectiveManager(fc *legatus.FlowCtx) (PkgManager, error) {
90
	if p.Manager != "" {
91
		return p.Manager, nil
92
	}
93
	for _, c := range []struct {
94
		bin string
95
		mgr PkgManager
96
	}{{"pacman", Pacman}, {"apt-get", Apt}, {"dnf", Dnf}} {
97
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{Path: "sh", Args: []string{"-c", "command -v " + c.bin}})
98
		if err != nil {
99
			return "", culpa.WithCode(culpa.Wrapf(err, "probe %s", c.bin), "PKG_MANAGER")
100
		}
101
		if res.ExitCode == 0 {
102
			return c.mgr, nil
103
		}
104
	}
105
	return "", culpa.WithCode(
106
		culpa.Errorf("package %s: no supported manager (pacman/apt-get/dnf) on host", p.Pkg), "PKG_MANAGER")
107
}
108
109
// isInstalled queries the manager for the package's presence.
110
func (p Package) isInstalled(fc *legatus.FlowCtx) (bool, error) {
111
	mgr, err := p.effectiveManager(fc)
112
	if err != nil {
113
		return false, err
114
	}
115
	switch mgr {
116
	case Apt:
117
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
118
			Path: "dpkg-query",
119
			Args: []string{"-W", "-f=${Status}", p.Pkg},
120
		})
121
		if err != nil {
122
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
123
		}
124
		if res.ExitCode != 0 {
125
			return false, nil
126
		}
127
		return strings.Contains(string(res.Stdout), "install ok installed"), nil
128
	case Dnf:
129
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
130
			Path: "rpm",
131
			Args: []string{"-q", p.Pkg},
132
		})
133
		if err != nil {
134
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
135
		}
136
		return res.ExitCode == 0, nil
137
	case Pacman:
138
		res, err := fc.Conn().Exec(fc.Ctx(), legatus.Cmd{
139
			Path: "pacman",
140
			Args: []string{"-Q", p.Pkg},
141
		})
142
		if err != nil {
143
			return false, culpa.WithCode(culpa.Wrapf(err, "query %s", p.Pkg), "PKG_PROBE")
144
		}
145
		return res.ExitCode == 0, nil
146
	default:
147
		return false, culpa.WithCode(culpa.Errorf("package %s: unknown manager %q", p.Pkg, mgr), "PKG_MANAGER")
148
	}
149
}
150
151
// applyCmd builds the install/remove command for the resolved manager and state,
152
// sudo-wrapped per the resolved sudo decision.
153
func (p Package) applyCmd(mgr PkgManager, sudo bool) (legatus.Cmd, error) {
154
	var path string
155
	switch mgr {
156
	case Apt:
157
		path = "apt-get"
158
	case Dnf:
159
		path = "dnf"
160
	case Pacman:
161
		verb := "-S" // present
162
		if p.State == PkgAbsent {
163
			verb = "-R"
164
		}
165
		return legatus.Cmd{Path: "pacman", Args: []string{verb, "--noconfirm", p.Pkg}, Sudo: sudo}, nil
166
	default:
167
		return legatus.Cmd{}, culpa.WithCode(culpa.Errorf("package %s: unknown manager %q", p.Pkg, mgr), "PKG_MANAGER")
168
	}
169
170
	var verb string
171
	switch p.State {
172
	case PkgPresent:
173
		verb = "install"
174
	case PkgAbsent:
175
		verb = "remove"
176
	default:
177
		return legatus.Cmd{}, culpa.WithCode(culpa.Errorf("package %s: unknown state %q", p.Pkg, p.State), "PKG_STATE")
178
	}
179
180
	return legatus.Cmd{
181
		Path: path,
182
		Args: []string{"-y", verb, p.Pkg},
183
		Env:  map[string]string{"DEBIAN_FRONTEND": "noninteractive"},
184
		Sudo: sudo,
185
	}, nil
186
}
187

Source Files