issue 1841/S9: carry out the upgrade's root acts through a root helper on a socket only the keeper opens, so no sudo rule lets the keeper's account run anything as root (ADR 0510)
This commit is contained in:
@@ -181,26 +181,33 @@ func keeperHooks(k *Keeper) Hooks {
|
||||
}
|
||||
}
|
||||
|
||||
// newRootActs is the client of the module's root helper (ADR 0510), which carries out the acts that need root;
|
||||
// the helper's socket client replaces it. Until then every act is refused, named.
|
||||
var newRootActs = func() RootActs { return noRoot{} }
|
||||
// newRootActs is the client of the module's root helper (ADR 0510), which carries out the acts that need root,
|
||||
// at the socket the manifest names to the keeper; with none named, every act is refused, named.
|
||||
var newRootActs = func() RootActs {
|
||||
if sock := os.Getenv(EnvRootSocket); sock != "" {
|
||||
return rootClient{Socket: sock}
|
||||
}
|
||||
return noRoot{}
|
||||
}
|
||||
|
||||
type noRoot struct{}
|
||||
|
||||
const noRootSaid = "the keeper was started without " + EnvRootSocket + ", the root helper's socket"
|
||||
|
||||
func (noRoot) Transaction(kind, run string, ignore []string) error {
|
||||
return errors.New("the root helper's client is not built in yet: no transaction can start")
|
||||
return errors.New(noRootSaid + ": no transaction can start")
|
||||
}
|
||||
|
||||
func (noRoot) Journal(unit string, n int) ([]string, error) {
|
||||
return nil, errors.New("the root helper's client is not built in yet: no journal can be read")
|
||||
return nil, errors.New(noRootSaid + ": no journal can be read")
|
||||
}
|
||||
|
||||
func (noRoot) Rollback(run string, plan []RollbackItem) error {
|
||||
return errors.New("the root helper's client is not built in yet: no rollback can start")
|
||||
return errors.New(noRootSaid + ": no rollback can start")
|
||||
}
|
||||
|
||||
func (noRoot) Maps() ([]Mapped, error) {
|
||||
return nil, errors.New("the root helper's client is not built in yet: no process's maps can be read")
|
||||
return nil, errors.New(noRootSaid + ": no process's maps can be read")
|
||||
}
|
||||
|
||||
// Keeper is the module's process on one node.
|
||||
|
||||
@@ -73,8 +73,9 @@ func TestTheKeeperIsAProcessUnderItsOwnAccountWithABusAccountOfItsOwn(t *testing
|
||||
t.Errorf("the keeper runs as its own account, never root (ADR 0508 §8): user %v", k["user"])
|
||||
}
|
||||
env, _ := k["env"].(map[string]any)
|
||||
if len(env) != 2 || env[EnvBroker] != "${dir:state}/broker-keeper.secret" || env[EnvConfig] != "${dir:state}/keeper.json" {
|
||||
t.Errorf("the keeper's environment names its account file and its config file: %v", env)
|
||||
if len(env) != 3 || env[EnvBroker] != "${dir:state}/broker-keeper.secret" || env[EnvConfig] != "${dir:state}/keeper.json" ||
|
||||
env[EnvRootSocket] != RootSocketPath {
|
||||
t.Errorf("the keeper's environment names its account file, its config file and the root helper's socket: %v", env)
|
||||
}
|
||||
secrets, _ := rawManifest(t)["own-secrets"].(map[string]any)
|
||||
broker, _ := secrets["broker-keeper"].(map[string]any)
|
||||
|
||||
@@ -21,9 +21,18 @@ const binaryName = "pacman-tools"
|
||||
func bg() context.Context { return context.Background() }
|
||||
|
||||
func main() {
|
||||
// `pacman-tools keeper` is the module's process (keeper.go); without a word it serves the tools.
|
||||
if len(os.Args) > 1 && os.Args[1] == "keeper" {
|
||||
os.Exit(keeper())
|
||||
// `pacman-tools keeper` is the module's process (keeper.go), `root` its root helper (root.go); without a
|
||||
// word it serves the tools.
|
||||
if len(os.Args) > 1 {
|
||||
switch os.Args[1] {
|
||||
case "keeper":
|
||||
os.Exit(keeper())
|
||||
case "root":
|
||||
// the root helper (root.go, ADR 0510), and the command of a rollback's unit it starts
|
||||
os.Exit(rootHelper())
|
||||
case "root-rollback":
|
||||
os.Exit(rootRollback(os.Args[2:]))
|
||||
}
|
||||
}
|
||||
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): pacman.
|
||||
if err := stdio.Serve("", tools(ThisMachine())); err != nil {
|
||||
|
||||
@@ -0,0 +1,506 @@
|
||||
package main
|
||||
|
||||
// The root helper (novox/hq ADR 0510): the module's acts that need root go through a second process of the same
|
||||
// build, `pacman-root`, running as root with no bus account, never through a sudo rule. It serves a closed list
|
||||
// of verbs on a unix socket only the keeper's account opens, the peer's account checked on every connection:
|
||||
//
|
||||
// - transaction: start a run's full upgrade as a unit of the service manager, named by the run, leaving out
|
||||
// the packages the run holds back;
|
||||
// - rollback: start a rollback's transaction as such a unit, carrying out the plan sent with the verb;
|
||||
// - maps: the processes still mapping a file an upgrade replaced, by unit or container and executable;
|
||||
// - journal: the last lines a transaction unit of the module logged.
|
||||
//
|
||||
// Every other verb is refused, and every argument that is not a unit name of the module's own transactions, a
|
||||
// package name, a version, a run's id, or a plan item's location in the package cache or the Arch Linux
|
||||
// Archive. The helper's program is root's (ADR 0510 §3): the node-engine places a process's program in a
|
||||
// directory its own account owns, and that account is root.
|
||||
//
|
||||
// A rollback's unit runs this same program (`pacman-tools root-rollback <plan>`) on a plan file the helper
|
||||
// wrote into a directory only root reads: it finds each package of the dated repository by its file's name,
|
||||
// installs the plan's packages in one pacman transaction, then removes the ones the rollback was asked to
|
||||
// remove. Nothing is changed when a package cannot be found.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// The helper's verbs.
|
||||
const (
|
||||
VerbTransaction = "transaction"
|
||||
VerbRollback = "rollback"
|
||||
VerbMaps = "maps"
|
||||
VerbJournal = "journal"
|
||||
)
|
||||
|
||||
// The helper's environment, and the keeper's word for where the helper listens.
|
||||
const (
|
||||
EnvRootSocket = "PACMAN_ROOT_SOCKET" // the helper's socket, for the helper and the keeper
|
||||
EnvRootPeer = "PACMAN_ROOT_PEER" // the only account the helper answers: the keeper's
|
||||
// RootSocketPath is the socket as the manifest names it: in a directory of the module's state the helper
|
||||
// makes, root's.
|
||||
RootSocketPath = "${dir:state}/root/helper.sock"
|
||||
)
|
||||
|
||||
// rootTimeout bounds one exchange with the helper: every verb answers at once (a transaction is started, not
|
||||
// waited for).
|
||||
const rootTimeout = 30 * time.Second
|
||||
|
||||
// mostJournalLines is the most lines the journal verb reads.
|
||||
const mostJournalLines = 400
|
||||
|
||||
// RootRequest is one verb sent to the helper, one per connection.
|
||||
type RootRequest struct {
|
||||
Verb string `json:"verb"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Run string `json:"run,omitempty"`
|
||||
Ignore []string `json:"ignore,omitempty"`
|
||||
Plan []RollbackItem `json:"plan,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
Lines int `json:"lines,omitempty"`
|
||||
}
|
||||
|
||||
// RootAnswer is the helper's answer: an error, or what the verb read.
|
||||
type RootAnswer struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
Lines []string `json:"lines,omitempty"`
|
||||
Mapped []Mapped `json:"mapped,omitempty"`
|
||||
}
|
||||
|
||||
// RootHelper is the helper: what it runs, the process table it reads, where it keeps plans, its own program,
|
||||
// and the one account it answers.
|
||||
type RootHelper struct {
|
||||
Run Runner
|
||||
Proc ProcFS
|
||||
ProcRoot string
|
||||
PlanDir string
|
||||
Self string
|
||||
Peer int
|
||||
// PeerOf is the account at the other end of a connection; nil reads it from the socket (SO_PEERCRED).
|
||||
PeerOf func(net.Conn) (int, error)
|
||||
Logf func(format string, a ...any)
|
||||
}
|
||||
|
||||
var (
|
||||
// runIDShape is a run's id (RunID): its start in UTC to the second.
|
||||
runIDShape = regexp.MustCompile(`^[0-9]{8}T[0-9]{6}Z$`)
|
||||
// ownUnit is a transaction unit of the module's own runs (TransactionUnit).
|
||||
ownUnit = regexp.MustCompile(`^` + unitPrefix + `(` + RunUpgrade + `|` + RunRollback + `)-[0-9]{8}t[0-9]{6}z$`)
|
||||
// archArg is a package's architecture in a file's name.
|
||||
archArg = regexp.MustCompile(`^[a-z0-9_]+$`)
|
||||
// datedRepository is the archive's repository as it stood on one day, its architecture left to pacman.
|
||||
datedRepository = regexp.MustCompile(`^` + regexp.QuoteMeta(ArchiveURL) + `/repos/[0-9]{4}/[0-9]{2}/[0-9]{2}/[A-Za-z0-9_-]+/os/\$arch$`)
|
||||
)
|
||||
|
||||
func refused(format string, a ...any) RootAnswer {
|
||||
return RootAnswer{Error: "refused: " + fmt.Sprintf(format, a...)}
|
||||
}
|
||||
|
||||
func checkRun(run string) error {
|
||||
if !runIDShape.MatchString(run) {
|
||||
return fmt.Errorf("%q is not a run's id", run)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkItem refuses a plan item that is not a package brought back from the cache or the archive, or removed.
|
||||
func checkItem(i RollbackItem) error {
|
||||
if !packageName.MatchString(i.Name) {
|
||||
return fmt.Errorf("%q is not a package's name", i.Name)
|
||||
}
|
||||
if i.Source == SourceRemoved {
|
||||
if i.To != "" || i.Location != "" {
|
||||
return fmt.Errorf("%s is removed, and names a version or a place to take it from", i.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if !packageVersion.MatchString(i.To) {
|
||||
return fmt.Errorf("%q is not a version of %s", i.To, i.Name)
|
||||
}
|
||||
switch i.Source {
|
||||
case SourceCache:
|
||||
f := cacheFile.FindStringSubmatch(path.Base(i.Location))
|
||||
if path.Clean(i.Location) != i.Location || path.Dir(i.Location) != CacheDir || f == nil || f[1] != i.Name || f[2] != i.To {
|
||||
return fmt.Errorf("%q is not %s %s's file in the package cache", i.Location, i.Name, i.To)
|
||||
}
|
||||
case SourceArchive:
|
||||
prefix := ArchiveURL + "/packages/" + i.Name[:1] + "/" + i.Name + "/" + i.Name + "-" + i.To + "-"
|
||||
arch, ok := strings.CutPrefix(i.Location, prefix)
|
||||
if !ok || !strings.HasSuffix(arch, ".pkg.tar.zst") || !archArg.MatchString(strings.TrimSuffix(arch, ".pkg.tar.zst")) {
|
||||
return fmt.Errorf("%q is not %s %s's file in the archive", i.Location, i.Name, i.To)
|
||||
}
|
||||
case SourceRepository:
|
||||
if !datedRepository.MatchString(i.Location) {
|
||||
return fmt.Errorf("%q is not a dated repository of the archive", i.Location)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("%q is not a source a package is brought back from", i.Source)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Handle carries out one verb, or refuses it naming why; nothing runs for a refused verb.
|
||||
func (h *RootHelper) Handle(req RootRequest) RootAnswer {
|
||||
switch req.Verb {
|
||||
case VerbTransaction:
|
||||
if req.Kind != RunUpgrade {
|
||||
return refused("a transaction is a full upgrade, not %q", req.Kind)
|
||||
}
|
||||
if err := checkRun(req.Run); err != nil {
|
||||
return refused("%v", err)
|
||||
}
|
||||
for _, n := range req.Ignore {
|
||||
if !packageName.MatchString(n) {
|
||||
return refused("%q is not a package's name to leave out", n)
|
||||
}
|
||||
}
|
||||
args := append([]string{"pacman"}, UpgradeArgs...)
|
||||
if len(req.Ignore) > 0 {
|
||||
args = append(args, "--ignore", strings.Join(req.Ignore, ","))
|
||||
}
|
||||
return h.startUnit(TransactionUnit(RunUpgrade, req.Run), "pacman full upgrade of run "+req.Run, args)
|
||||
case VerbRollback:
|
||||
if err := checkRun(req.Run); err != nil {
|
||||
return refused("%v", err)
|
||||
}
|
||||
if len(req.Plan) == 0 {
|
||||
return refused("a rollback with no plan")
|
||||
}
|
||||
for _, i := range req.Plan {
|
||||
if err := checkItem(i); err != nil {
|
||||
return refused("%v", err)
|
||||
}
|
||||
}
|
||||
raw, err := json.Marshal(req.Plan)
|
||||
if err != nil {
|
||||
return RootAnswer{Error: err.Error()}
|
||||
}
|
||||
if err := os.MkdirAll(h.PlanDir, 0o700); err != nil {
|
||||
return RootAnswer{Error: "the plan's directory: " + err.Error()}
|
||||
}
|
||||
file := filepath.Join(h.PlanDir, req.Run+".json")
|
||||
if err := os.WriteFile(file, raw, 0o600); err != nil {
|
||||
return RootAnswer{Error: "writing the plan: " + err.Error()}
|
||||
}
|
||||
return h.startUnit(TransactionUnit(RunRollback, req.Run), "pacman rollback of run "+req.Run, []string{h.Self, "root-rollback", file})
|
||||
case VerbMaps:
|
||||
mapped, err := MappedDeleted(h.Proc, h.ProcRoot)
|
||||
if err != nil {
|
||||
return RootAnswer{Error: "reading the process table: " + err.Error()}
|
||||
}
|
||||
return RootAnswer{Mapped: mapped}
|
||||
case VerbJournal:
|
||||
if !ownUnit.MatchString(req.Unit) {
|
||||
return refused("%q is not a transaction unit of the module", req.Unit)
|
||||
}
|
||||
if req.Lines < 1 || req.Lines > mostJournalLines {
|
||||
return refused("a journal is read 1 to %d lines at a time, not %d", mostJournalLines, req.Lines)
|
||||
}
|
||||
r := h.Run(context.Background(), "journalctl", "--no-pager", "-o", "cat", "-n", strconv.Itoa(req.Lines), "-u", req.Unit)
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return RootAnswer{Error: failure("journalctl", "journalctl", r).Error()}
|
||||
}
|
||||
return RootAnswer{Lines: lines(r.Stdout)}
|
||||
}
|
||||
return refused("%q is not a verb of the root helper (%s, %s, %s, %s)", req.Verb, VerbTransaction, VerbRollback, VerbMaps, VerbJournal)
|
||||
}
|
||||
|
||||
// startUnit starts command as a transient unit of the service manager and answers once it started.
|
||||
func (h *RootHelper) startUnit(unit, description string, command []string) RootAnswer {
|
||||
args := append([]string{"--unit=" + unit, "--description=" + description + ", started by the mesh's pacman keeper", "--quiet"}, command...)
|
||||
r := h.Run(context.Background(), "systemd-run", args...)
|
||||
if r.Status != 0 || r.Err != "" {
|
||||
return RootAnswer{Error: failure("systemd-run", "systemd-run", r).Error()}
|
||||
}
|
||||
return RootAnswer{}
|
||||
}
|
||||
|
||||
// peerCred is the account at the other end of a unix socket.
|
||||
func peerCred(c net.Conn) (int, error) {
|
||||
uc, ok := c.(*net.UnixConn)
|
||||
if !ok {
|
||||
return -1, errors.New("not a unix socket")
|
||||
}
|
||||
raw, err := uc.SyscallConn()
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
uid := -1
|
||||
var gerr error
|
||||
if err := raw.Control(func(fd uintptr) {
|
||||
cred, err := unix.GetsockoptUcred(int(fd), unix.SOL_SOCKET, unix.SO_PEERCRED)
|
||||
if err != nil {
|
||||
gerr = err
|
||||
return
|
||||
}
|
||||
uid = int(cred.Uid)
|
||||
}); err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return uid, gerr
|
||||
}
|
||||
|
||||
// Serve answers each connection on l, one request each, until ctx ends: a peer that is not the keeper's
|
||||
// account is told so and nothing it sent is read.
|
||||
func (h *RootHelper) Serve(ctx context.Context, l net.Listener) error {
|
||||
go func() { <-ctx.Done(); l.Close() }()
|
||||
peerOf := h.PeerOf
|
||||
if peerOf == nil {
|
||||
peerOf = peerCred
|
||||
}
|
||||
for {
|
||||
c, err := l.Accept()
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
go h.serveOne(c, peerOf)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RootHelper) serveOne(c net.Conn, peerOf func(net.Conn) (int, error)) {
|
||||
defer c.Close()
|
||||
_ = c.SetDeadline(time.Now().Add(rootTimeout))
|
||||
answer := func(a RootAnswer) { _ = json.NewEncoder(c).Encode(a) }
|
||||
uid, err := peerOf(c)
|
||||
if err != nil || uid != h.Peer {
|
||||
if h.Logf != nil {
|
||||
h.Logf("[pacman] root helper: a connection from account %d refused: only the keeper's (%d) is answered", uid, h.Peer)
|
||||
}
|
||||
answer(refused("only the keeper's account is answered"))
|
||||
return
|
||||
}
|
||||
var req RootRequest
|
||||
if err := json.NewDecoder(io.LimitReader(c, 4<<20)).Decode(&req); err != nil {
|
||||
answer(refused("no request: %v", err))
|
||||
return
|
||||
}
|
||||
a := h.Handle(req)
|
||||
if h.Logf != nil {
|
||||
said := "carried out"
|
||||
if a.Error != "" {
|
||||
said = a.Error
|
||||
}
|
||||
h.Logf("[pacman] root helper: %s %s %s: %s", req.Verb, req.Kind, req.Run+req.Unit, said)
|
||||
}
|
||||
answer(a)
|
||||
}
|
||||
|
||||
// rootClient is the keeper's side: the RootActs over the helper's socket.
|
||||
type rootClient struct{ Socket string }
|
||||
|
||||
func (c rootClient) ask(req RootRequest) (RootAnswer, error) {
|
||||
conn, err := net.DialTimeout("unix", c.Socket, rootTimeout)
|
||||
if err != nil {
|
||||
return RootAnswer{}, fmt.Errorf("the root helper cannot be reached at %s: %w", c.Socket, err)
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(rootTimeout))
|
||||
if err := json.NewEncoder(conn).Encode(req); err != nil {
|
||||
return RootAnswer{}, fmt.Errorf("asking the root helper: %w", err)
|
||||
}
|
||||
var a RootAnswer
|
||||
if err := json.NewDecoder(bufio.NewReader(conn)).Decode(&a); err != nil {
|
||||
return RootAnswer{}, fmt.Errorf("the root helper's answer: %w", err)
|
||||
}
|
||||
if a.Error != "" {
|
||||
return a, fmt.Errorf("the root helper: %s", a.Error)
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (c rootClient) Transaction(kind, run string, ignore []string) error {
|
||||
_, err := c.ask(RootRequest{Verb: VerbTransaction, Kind: kind, Run: run, Ignore: ignore})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c rootClient) Rollback(run string, plan []RollbackItem) error {
|
||||
_, err := c.ask(RootRequest{Verb: VerbRollback, Run: run, Plan: plan})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c rootClient) Journal(unit string, n int) ([]string, error) {
|
||||
a, err := c.ask(RootRequest{Verb: VerbJournal, Unit: unit, Lines: n})
|
||||
return a.Lines, err
|
||||
}
|
||||
|
||||
func (c rootClient) Maps() ([]Mapped, error) {
|
||||
a, err := c.ask(RootRequest{Verb: VerbMaps})
|
||||
if a.Mapped == nil && err == nil {
|
||||
a.Mapped = []Mapped{}
|
||||
}
|
||||
return a.Mapped, err
|
||||
}
|
||||
|
||||
// rootHelper is `pacman-tools root`: it listens on its socket, made the keeper's alone, until stopped.
|
||||
func rootHelper() int {
|
||||
logf := func(f string, a ...any) { fmt.Fprintf(os.Stderr, f+"\n", a...) }
|
||||
if os.Getuid() != 0 {
|
||||
logf("[pacman] root helper: it runs as root, and runs as account %d", os.Getuid())
|
||||
return 2
|
||||
}
|
||||
sock, peerName := os.Getenv(EnvRootSocket), os.Getenv(EnvRootPeer)
|
||||
if sock == "" || peerName == "" {
|
||||
logf("[pacman] root helper was started without %s or %s", EnvRootSocket, EnvRootPeer)
|
||||
return 2
|
||||
}
|
||||
peer, err := user.Lookup(peerName)
|
||||
if err != nil {
|
||||
logf("[pacman] root helper: the keeper's account %s: %v", peerName, err)
|
||||
return 1
|
||||
}
|
||||
uid, err := strconv.Atoi(peer.Uid)
|
||||
if err != nil {
|
||||
logf("[pacman] root helper: account %s has no number: %v", peerName, err)
|
||||
return 1
|
||||
}
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
logf("[pacman] root helper: its own program: %v", err)
|
||||
return 1
|
||||
}
|
||||
dir := filepath.Dir(sock)
|
||||
if err := os.MkdirAll(dir, 0o711); err != nil || os.Chown(dir, 0, 0) != nil || os.Chmod(dir, 0o711) != nil {
|
||||
logf("[pacman] root helper: its directory %s cannot be made root's: %v", dir, err)
|
||||
return 1
|
||||
}
|
||||
_ = os.Remove(sock)
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
logf("[pacman] root helper: listening on %s: %v", sock, err)
|
||||
return 1
|
||||
}
|
||||
if err := os.Chown(sock, uid, 0); err != nil || os.Chmod(sock, 0o600) != nil {
|
||||
logf("[pacman] root helper: the socket cannot be made the keeper's alone: %v", err)
|
||||
return 1
|
||||
}
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
h := &RootHelper{Run: ExecRunner, Proc: osProc{}, ProcRoot: "/proc", PlanDir: filepath.Join(dir, "plans"), Self: self, Peer: uid, Logf: logf}
|
||||
logf("[pacman] root helper listening on %s for account %s (%d)", sock, peerName, uid)
|
||||
if err := h.Serve(ctx, l); err != nil {
|
||||
logf("[pacman] root helper stopped: %v", err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// machineArch is this machine's architecture as Arch Linux names it.
|
||||
func machineArch() string {
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
return "x86_64"
|
||||
case "arm64":
|
||||
return "aarch64"
|
||||
}
|
||||
return runtime.GOARCH
|
||||
}
|
||||
|
||||
// archiveHas says whether the archive holds a file, by asking for its head.
|
||||
func archiveHas(url string) bool {
|
||||
c := &http.Client{Timeout: 30 * time.Second}
|
||||
resp, err := c.Head(url)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
// rollbackUnit carries out a rollback plan inside its unit: every package of a dated repository found by its
|
||||
// file's name (this machine's architecture, then any), the plan's packages installed in one transaction, then
|
||||
// the packages to remove removed. Nothing is changed when a package is not found.
|
||||
func rollbackUnit(file, arch string, run func(string, ...string) int, probe func(string) bool, log io.Writer) int {
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
fmt.Fprintf(log, "error: reading the plan: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
var plan []RollbackItem
|
||||
if err := json.Unmarshal(raw, &plan); err != nil {
|
||||
fmt.Fprintf(log, "error: the plan is not one: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
var targets, remove []string
|
||||
for _, i := range plan {
|
||||
if err := checkItem(i); err != nil {
|
||||
fmt.Fprintf(log, "error: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
switch i.Source {
|
||||
case SourceRemoved:
|
||||
remove = append(remove, i.Name)
|
||||
case SourceRepository:
|
||||
base := strings.Replace(i.Location, "$arch", arch, 1)
|
||||
found := ""
|
||||
for _, a := range []string{arch, "any"} {
|
||||
if u := base + "/" + i.Name + "-" + i.To + "-" + a + ".pkg.tar.zst"; probe(u) {
|
||||
found = u
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == "" {
|
||||
fmt.Fprintf(log, "error: %s %s is not in %s: nothing was changed\n", i.Name, i.To, base)
|
||||
return 1
|
||||
}
|
||||
targets = append(targets, found)
|
||||
default:
|
||||
targets = append(targets, i.Location)
|
||||
}
|
||||
}
|
||||
if len(targets) > 0 {
|
||||
if code := run("pacman", append([]string{"-U", "--noconfirm", "--"}, targets...)...); code != 0 {
|
||||
return code
|
||||
}
|
||||
}
|
||||
if len(remove) > 0 {
|
||||
return run("pacman", append([]string{"-R", "--noconfirm", "--"}, remove...)...)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// rootRollback is `pacman-tools root-rollback <plan>`, the rollback unit's command: pacman's output goes to the
|
||||
// unit's journal.
|
||||
func rootRollback(args []string) int {
|
||||
if len(args) != 1 || os.Getuid() != 0 {
|
||||
fmt.Fprintln(os.Stderr, "error: root-rollback takes one plan file, and runs as root in its unit")
|
||||
return 2
|
||||
}
|
||||
run := func(name string, a ...string) int {
|
||||
cmd := exec.Command(name, a...)
|
||||
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
|
||||
cmd.Env = append(os.Environ(), "LC_ALL=C")
|
||||
if err := cmd.Run(); err != nil {
|
||||
var exit *exec.ExitError
|
||||
if errors.As(err, &exit) {
|
||||
return exit.ExitCode()
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "error: %s: %v\n", name, err)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
return rollbackUnit(args[0], machineArch(), run, archiveHas, os.Stderr)
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
package main
|
||||
|
||||
// The root helper (novox/hq ADR 0510; acceptance 1841/AC10 as reworded on the issue): each listed verb with valid
|
||||
// arguments is carried out; every other verb, a unit name outside the module's own, a package name with other
|
||||
// characters and a run of no run's shape are refused; a connection from an account other than the keeper's is
|
||||
// refused; the manifest declares `pacman-root` as root with no bus and places nothing under the sudo
|
||||
// configuration.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// rootBed is a helper over a recording runner, a stand-in process table and a plan directory of its own.
|
||||
type rootBed struct {
|
||||
h *RootHelper
|
||||
ran [][]string
|
||||
}
|
||||
|
||||
func newRootBed(t *testing.T) *rootBed {
|
||||
b := &rootBed{}
|
||||
b.h = &RootHelper{
|
||||
Run: func(_ context.Context, name string, args ...string) Ran {
|
||||
b.ran = append(b.ran, append([]string{name}, args...))
|
||||
if name == "journalctl" {
|
||||
return Ran{Stdout: "error: failed to commit transaction\n"}
|
||||
}
|
||||
return Ran{}
|
||||
},
|
||||
Proc: stubProc{"/proc": {"1"}, "/proc/1/maps": {"7f00 r-xp 0 0:1 1 /usr/lib/libc.so.6 (deleted)"}, "/proc/1/cgroup": {"0::/system.slice/sshd.service"}, "/proc/1/exe": {"/usr/bin/sshd"}},
|
||||
ProcRoot: "/proc",
|
||||
PlanDir: t.TempDir(),
|
||||
Self: "/var/lib/mesh/daemons/pacman-root/pacman-tools",
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// stubProc is a process table in memory: a directory lists its names, a file holds its one line, a link its target.
|
||||
type stubProc map[string][]string
|
||||
|
||||
func (s stubProc) ReadDir(dir string) ([]string, error) { return s[dir], nil }
|
||||
func (s stubProc) ReadFile(name string) ([]byte, error) {
|
||||
v, ok := s[name]
|
||||
if !ok {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return []byte(strings.Join(v, "\n")), nil
|
||||
}
|
||||
func (s stubProc) Readlink(name string) (string, error) {
|
||||
v, ok := s[name]
|
||||
if !ok {
|
||||
return "", os.ErrNotExist
|
||||
}
|
||||
return v[0], nil
|
||||
}
|
||||
|
||||
const aRun = "20261024T040000Z"
|
||||
|
||||
// testSocket is a socket in the abstract namespace, named for the test: a path under a test's temporary
|
||||
// directory can pass the 108 bytes a socket's path may hold.
|
||||
func testSocket(t *testing.T) string {
|
||||
return fmt.Sprintf("@pacman-root-test-%d-%s", os.Getpid(), t.Name())
|
||||
}
|
||||
|
||||
func TestTheRootHelperStartsAnUpgradeAsTheRunsUnit(t *testing.T) {
|
||||
b := newRootBed(t)
|
||||
a := b.h.Handle(RootRequest{Verb: VerbTransaction, Kind: RunUpgrade, Run: aRun, Ignore: []string{"linux", "linux-headers"}})
|
||||
if a.Error != "" {
|
||||
t.Fatalf("refused: %s", a.Error)
|
||||
}
|
||||
want := []string{"systemd-run", "--unit=" + TransactionUnit(RunUpgrade, aRun), "--description=pacman full upgrade of run " + aRun + ", started by the mesh's pacman keeper",
|
||||
"--quiet", "pacman", "-Syu", "--noconfirm", "--ignore", "linux,linux-headers"}
|
||||
if len(b.ran) != 1 || !reflect.DeepEqual(b.ran[0], want) {
|
||||
t.Fatalf("ran %q, want %q", b.ran, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperStartsARollbackWithThePlanInTheRequest(t *testing.T) {
|
||||
b := newRootBed(t)
|
||||
plan := []RollbackItem{
|
||||
{Name: "bash", From: "5.3.20-1", To: "5.3.15-1", Source: SourceCache, Location: CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst"},
|
||||
{Name: "glibc", From: "2.43-2", To: "2.43-1", Source: SourceArchive, Location: ArchiveURL + "/packages/g/glibc/glibc-2.43-1-x86_64.pkg.tar.zst"},
|
||||
{Name: "zlib", From: "1.3.2-1", To: "1.3.1-2", Source: SourceRepository, Location: ArchiveURL + "/repos/2026/10/04/core/os/$arch"},
|
||||
{Name: "made-up-library", From: "1.0-1", Source: SourceRemoved},
|
||||
}
|
||||
a := b.h.Handle(RootRequest{Verb: VerbRollback, Run: aRun, Plan: plan})
|
||||
if a.Error != "" {
|
||||
t.Fatalf("refused: %s", a.Error)
|
||||
}
|
||||
file := filepath.Join(b.h.PlanDir, aRun+".json")
|
||||
want := []string{"systemd-run", "--unit=" + TransactionUnit(RunRollback, aRun), "--description=pacman rollback of run " + aRun + ", started by the mesh's pacman keeper",
|
||||
"--quiet", b.h.Self, "root-rollback", file}
|
||||
if len(b.ran) != 1 || !reflect.DeepEqual(b.ran[0], want) {
|
||||
t.Fatalf("ran %q, want %q", b.ran, want)
|
||||
}
|
||||
raw, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []RollbackItem
|
||||
if err := json.Unmarshal(raw, &got); err != nil || !reflect.DeepEqual(got, plan) {
|
||||
t.Fatalf("the plan file holds %s (%v)", raw, err)
|
||||
}
|
||||
if fi, _ := os.Stat(file); fi.Mode().Perm() != 0o600 {
|
||||
t.Errorf("the plan file is mode %v, want 0600: only root reads what root carries out", fi.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperReadsTheReplacedFilesMappings(t *testing.T) {
|
||||
b := newRootBed(t)
|
||||
a := b.h.Handle(RootRequest{Verb: VerbMaps})
|
||||
want := []Mapped{{Unit: "sshd.service", Executable: "/usr/bin/sshd"}}
|
||||
if a.Error != "" || !reflect.DeepEqual(a.Mapped, want) {
|
||||
t.Fatalf("maps answered %+v, want %+v", a, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperReadsATransactionUnitsJournal(t *testing.T) {
|
||||
b := newRootBed(t)
|
||||
unit := TransactionUnit(RunUpgrade, aRun)
|
||||
a := b.h.Handle(RootRequest{Verb: VerbJournal, Unit: unit, Lines: 40})
|
||||
if a.Error != "" || !reflect.DeepEqual(a.Lines, []string{"error: failed to commit transaction"}) {
|
||||
t.Fatalf("journal answered %+v", a)
|
||||
}
|
||||
want := []string{"journalctl", "--no-pager", "-o", "cat", "-n", "40", "-u", unit}
|
||||
if !reflect.DeepEqual(b.ran[0], want) {
|
||||
t.Fatalf("ran %q, want %q", b.ran[0], want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperRefusesEverythingOffItsList(t *testing.T) {
|
||||
good := RollbackItem{Name: "bash", From: "5.3.20-1", To: "5.3.15-1", Source: SourceCache, Location: CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst"}
|
||||
with := func(f func(*RollbackItem)) []RollbackItem { i := good; f(&i); return []RollbackItem{i} }
|
||||
cases := map[string]RootRequest{
|
||||
"another verb": {Verb: "shell", Run: aRun},
|
||||
"an empty verb": {},
|
||||
"another kind of transaction": {Verb: VerbTransaction, Kind: "remove", Run: aRun},
|
||||
"a rollback as a transaction": {Verb: VerbTransaction, Kind: RunRollback, Run: aRun},
|
||||
"a run of no run's shape": {Verb: VerbTransaction, Kind: RunUpgrade, Run: "../../etc"},
|
||||
"a run with an option in it": {Verb: VerbTransaction, Kind: RunUpgrade, Run: "--now"},
|
||||
"an ignored name with a space": {Verb: VerbTransaction, Kind: RunUpgrade, Run: aRun, Ignore: []string{"linux headers"}},
|
||||
"an ignored name as an option": {Verb: VerbTransaction, Kind: RunUpgrade, Run: aRun, Ignore: []string{"--root=/tmp"}},
|
||||
"an ignored name with a comma": {Verb: VerbTransaction, Kind: RunUpgrade, Run: aRun, Ignore: []string{"linux,glibc"}},
|
||||
"an ignored name in capitals": {Verb: VerbTransaction, Kind: RunUpgrade, Run: aRun, Ignore: []string{"Linux"}},
|
||||
"a journal of another unit": {Verb: VerbJournal, Unit: "sshd.service", Lines: 10},
|
||||
"a journal of a look-alike unit": {Verb: VerbJournal, Unit: "mesh-pacman-upgrade-x;reboot", Lines: 10},
|
||||
"a journal of too many lines": {Verb: VerbJournal, Unit: TransactionUnit(RunUpgrade, aRun), Lines: 100000},
|
||||
"a rollback of no plan": {Verb: VerbRollback, Run: aRun},
|
||||
"a rollback of a bad run": {Verb: VerbRollback, Run: "x", Plan: []RollbackItem{good}},
|
||||
"a plan item with a bad name": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.Name = "bash;rm" })},
|
||||
"a plan item with a bad version": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.To = "1 2" })},
|
||||
"a cache file outside the cache": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.Location = "/tmp/bash-5.3.15-1-x86_64.pkg.tar.zst" })},
|
||||
"a cache file climbing out": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.Location = CacheDir + "/../../../tmp/bash-5.3.15-1-x86_64.pkg.tar.zst" })},
|
||||
"a cache file of another package": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.Location = CacheDir + "/evil-1-1-x86_64.pkg.tar.zst" })},
|
||||
"an archive URL elsewhere": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) {
|
||||
i.Source, i.Location = SourceArchive, "https://example.org/packages/b/bash/bash-5.3.15-1-x86_64.pkg.tar.zst"
|
||||
})},
|
||||
"a repository URL elsewhere": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) {
|
||||
i.Source, i.Location = SourceRepository, ArchiveURL+"/repos/2026/10/04/../../../../x"
|
||||
})},
|
||||
"another source": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) { i.Source = "file" })},
|
||||
"a removal with a location": {Verb: VerbRollback, Run: aRun, Plan: with(func(i *RollbackItem) {
|
||||
i.Source, i.To = SourceRemoved, ""
|
||||
})},
|
||||
}
|
||||
for name, req := range cases {
|
||||
b := newRootBed(t)
|
||||
if a := b.h.Handle(req); a.Error == "" {
|
||||
t.Errorf("%s: carried out (%+v)", name, a)
|
||||
}
|
||||
if len(b.ran) != 0 {
|
||||
t.Errorf("%s: ran %q although refused", name, b.ran)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// serveOnce starts the helper on a socket of a test directory, allowing peer, and answers one request.
|
||||
func serveOnce(t *testing.T, peer int, req RootRequest) (RootAnswer, error) {
|
||||
t.Helper()
|
||||
b := newRootBed(t)
|
||||
b.h.Peer = peer
|
||||
sock := testSocket(t)
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
go func() { _ = b.h.Serve(ctx, l) }()
|
||||
return rootClient{Socket: sock}.ask(req)
|
||||
}
|
||||
|
||||
func TestTheRootHelperAnswersTheKeepersAccountOverItsSocket(t *testing.T) {
|
||||
a, err := serveOnce(t, os.Getuid(), RootRequest{Verb: VerbMaps})
|
||||
if err != nil || a.Error != "" || len(a.Mapped) != 1 {
|
||||
t.Fatalf("the keeper's own account was answered %+v, %v", a, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperRefusesAnyOtherAccount(t *testing.T) {
|
||||
a, err := serveOnce(t, os.Getuid()+1, RootRequest{Verb: VerbMaps})
|
||||
if err == nil && a.Error == "" {
|
||||
t.Fatalf("another account was answered: %+v", a)
|
||||
}
|
||||
if a.Mapped != nil {
|
||||
t.Fatalf("another account was handed the mappings: %+v", a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheClientCarriesTheRootActsAndSaysARefusal(t *testing.T) {
|
||||
b := newRootBed(t)
|
||||
b.h.Peer = os.Getuid()
|
||||
sock := testSocket(t)
|
||||
l, err := net.Listen("unix", sock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
go func() { _ = b.h.Serve(ctx, l) }()
|
||||
var acts RootActs = rootClient{Socket: sock}
|
||||
if err := acts.Transaction(RunUpgrade, aRun, nil); err != nil {
|
||||
t.Fatalf("transaction: %v", err)
|
||||
}
|
||||
if err := acts.Transaction(RunUpgrade, "bad", nil); err == nil || !strings.Contains(err.Error(), "run") {
|
||||
t.Fatalf("a bad run was not refused by name: %v", err)
|
||||
}
|
||||
if lines, err := acts.Journal(TransactionUnit(RunUpgrade, aRun), 5); err != nil || len(lines) != 1 {
|
||||
t.Fatalf("journal: %q, %v", lines, err)
|
||||
}
|
||||
if _, err := (rootClient{Socket: testSocket(t) + "-none"}).Maps(); err == nil || !strings.Contains(err.Error(), "root helper") {
|
||||
t.Fatalf("a helper that is not there is not said: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRootHelperIsRootWithNoBusAndNoSudoRule(t *testing.T) {
|
||||
m := manifest(t)
|
||||
r := m.resource(t, "root-helper")
|
||||
if r["type"] != "process" || r["name"] != "pacman-root" || r["artifact"] != "tools" ||
|
||||
strings.Join(strs(r["run"]), " ") != "./"+binaryName+" root" {
|
||||
t.Fatalf("the root helper: %v", r)
|
||||
}
|
||||
if _, ok := r["user"]; ok {
|
||||
t.Errorf("the root helper runs as root (ADR 0510 §1, §3), and names a user: %v", r["user"])
|
||||
}
|
||||
if _, ok := r["bus"]; ok {
|
||||
t.Errorf("the root helper has no bus account (ADR 0510 §1): %v", r["bus"])
|
||||
}
|
||||
env, _ := r["env"].(map[string]any)
|
||||
if env[EnvRootSocket] != RootSocketPath || env[EnvRootPeer] != "pacman-keeper" {
|
||||
t.Errorf("the root helper's environment names its socket and the keeper's account: %v", env)
|
||||
}
|
||||
k := m.resource(t, "keeper")
|
||||
kenv, _ := k["env"].(map[string]any)
|
||||
if kenv[EnvRootSocket] != RootSocketPath {
|
||||
t.Errorf("the keeper is told the helper's socket: %v", kenv)
|
||||
}
|
||||
for _, res := range m.Resources {
|
||||
if p, _ := res["path"].(string); strings.Contains(p, "sudoers") {
|
||||
t.Errorf("a file under the sudo configuration is placed: %s (ADR 0510 §1)", p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRollbackUnitInstallsThePlanThenRemovesWhatWasAskedInOrder(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
plan := []RollbackItem{
|
||||
{Name: "bash", To: "5.3.15-1", Source: SourceCache, Location: CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst"},
|
||||
{Name: "zlib", To: "1.3.1-2", Source: SourceRepository, Location: ArchiveURL + "/repos/2026/10/04/core/os/$arch"},
|
||||
{Name: "made-up-library", From: "1.0-1", Source: SourceRemoved},
|
||||
}
|
||||
raw, _ := json.Marshal(plan)
|
||||
file := filepath.Join(dir, aRun+".json")
|
||||
if err := os.WriteFile(file, raw, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var ran [][]string
|
||||
run := func(name string, args ...string) int { ran = append(ran, append([]string{name}, args...)); return 0 }
|
||||
probed := []string{}
|
||||
probe := func(url string) bool { probed = append(probed, url); return strings.HasSuffix(url, "-any.pkg.tar.zst") }
|
||||
if code := rollbackUnit(file, "x86_64", run, probe, os.Stderr); code != 0 {
|
||||
t.Fatalf("exit %d", code)
|
||||
}
|
||||
want := [][]string{
|
||||
{"pacman", "-U", "--noconfirm", "--", CacheDir + "/bash-5.3.15-1-x86_64.pkg.tar.zst", ArchiveURL + "/repos/2026/10/04/core/os/x86_64/zlib-1.3.1-2-any.pkg.tar.zst"},
|
||||
{"pacman", "-R", "--noconfirm", "--", "made-up-library"},
|
||||
}
|
||||
if !reflect.DeepEqual(ran, want) {
|
||||
t.Fatalf("ran %q, want %q", ran, want)
|
||||
}
|
||||
if len(probed) != 2 || !strings.HasSuffix(probed[0], "-x86_64.pkg.tar.zst") {
|
||||
t.Errorf("the machine's architecture is tried first, then any: %q", probed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheRollbackUnitChangesNothingWhenAPackageIsNotFound(t *testing.T) {
|
||||
file := filepath.Join(t.TempDir(), aRun+".json")
|
||||
raw, _ := json.Marshal([]RollbackItem{{Name: "zlib", To: "1.3.1-2", Source: SourceRepository, Location: ArchiveURL + "/repos/2026/10/04/core/os/$arch"}})
|
||||
_ = os.WriteFile(file, raw, 0o600)
|
||||
var ran [][]string
|
||||
run := func(name string, args ...string) int { ran = append(ran, append([]string{name}, args...)); return 0 }
|
||||
if code := rollbackUnit(file, "x86_64", run, func(string) bool { return false }, &strings.Builder{}); code == 0 || len(ran) != 0 {
|
||||
t.Fatalf("exit %d, ran %q: a package the archive does not hold fails the unit before any change", code, ran)
|
||||
}
|
||||
}
|
||||
@@ -164,6 +164,23 @@
|
||||
"template": "files/keeper.json",
|
||||
"trusted": true
|
||||
},
|
||||
{
|
||||
"id": "root-helper",
|
||||
"type": "process",
|
||||
"name": "pacman-root",
|
||||
"artifact": "tools",
|
||||
"run": [
|
||||
"./pacman-tools",
|
||||
"root"
|
||||
],
|
||||
"env": {
|
||||
"PACMAN_ROOT_SOCKET": "${dir:state}/root/helper.sock",
|
||||
"PACMAN_ROOT_PEER": "pacman-keeper"
|
||||
},
|
||||
"health": {
|
||||
"kind": "unit"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "keeper",
|
||||
"type": "process",
|
||||
@@ -176,7 +193,8 @@
|
||||
"user": "pacman-keeper",
|
||||
"env": {
|
||||
"PACMAN_KEEPER_BROKER": "${dir:state}/broker-keeper.secret",
|
||||
"PACMAN_KEEPER_CONFIG": "${dir:state}/keeper.json"
|
||||
"PACMAN_KEEPER_CONFIG": "${dir:state}/keeper.json",
|
||||
"PACMAN_ROOT_SOCKET": "${dir:state}/root/helper.sock"
|
||||
},
|
||||
"restart-on": [
|
||||
"keeper-config"
|
||||
|
||||
Reference in New Issue
Block a user