- syncup: podman first, docker only as fallback (podman wins on name clash;
existing docker units found in podman switch to podman)
- syncup: group podman-compose containers into one `kind: stack` unit per
project (project, working_dir, compose_files, compose_bin from labels);
disable existing member units ("part of stack <project>")
- generator: compose mode (podman-compose up/stop, WorkingDirectory) with
pod start/stop fallback when compose binary/dir/files are missing
- reconcile: stacks disabled with "stack not found" when neither compose
files nor pod exist
- root: scan root + rootless users listed in `users:` (podman only)
- non-root: config ~/.config/unitdore/units.yaml, own user units only
- sysuser package: runuser with XDG_RUNTIME_DIR/DBUS env, home/linger lookup
- install: warn when linger is not enabled for user units
- testdata/stack: podman-compose test stack and captured podman ps fixture
- docs: README, generated-units, man page, AGENTS, PLAN
96 lines
2.5 KiB
Go
96 lines
2.5 KiB
Go
// Package sysuser runs commands as the current process or as another user,
|
|
// and answers questions about local user accounts.
|
|
package sysuser
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
)
|
|
|
|
// IsRoot reports whether the process runs with euid 0.
|
|
func IsRoot() bool { return os.Geteuid() == 0 }
|
|
|
|
// Current returns the username of the effective user.
|
|
func Current() string {
|
|
u, err := user.LookupId(fmt.Sprint(os.Geteuid()))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return u.Username
|
|
}
|
|
|
|
// IsSelf reports whether name is the effective user (or empty).
|
|
func IsSelf(name string) bool {
|
|
return name == "" || name == Current()
|
|
}
|
|
|
|
// HomeDir returns the home directory of the given user.
|
|
func HomeDir(name string) (string, error) {
|
|
u, err := user.Lookup(name)
|
|
if err != nil {
|
|
return "", fmt.Errorf("looking up user %s: %w", name, err)
|
|
}
|
|
return u.HomeDir, nil
|
|
}
|
|
|
|
// UID returns the numeric uid of the given user as a string.
|
|
func UID(name string) (string, error) {
|
|
u, err := user.Lookup(name)
|
|
if err != nil {
|
|
return "", fmt.Errorf("looking up user %s: %w", name, err)
|
|
}
|
|
return u.Uid, nil
|
|
}
|
|
|
|
// Command builds a command that runs bin as the given user.
|
|
// An empty user, or the current user, runs it directly. Otherwise runuser is
|
|
// used (requires root) with XDG_RUNTIME_DIR pointed at the user's runtime dir,
|
|
// which rootless podman and systemctl --user need.
|
|
func Command(name string, bin string, args ...string) (*exec.Cmd, error) {
|
|
if IsSelf(name) {
|
|
return exec.Command(bin, args...), nil
|
|
}
|
|
if !IsRoot() {
|
|
return nil, fmt.Errorf("running as user %s requires root", name)
|
|
}
|
|
uid, err := UID(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
full := append([]string{"-u", name, "--", "env",
|
|
"XDG_RUNTIME_DIR=/run/user/" + uid,
|
|
"DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/" + uid + "/bus",
|
|
bin}, args...)
|
|
return exec.Command("runuser", full...), nil
|
|
}
|
|
|
|
// LookPath finds bin in PATH, then in the user's ~/.local/bin.
|
|
// Returns "" if not found.
|
|
func LookPath(name string, bin string) string {
|
|
if p, err := exec.LookPath(bin); err == nil {
|
|
return p
|
|
}
|
|
if name == "" {
|
|
name = Current()
|
|
}
|
|
home, err := HomeDir(name)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
p := filepath.Join(home, ".local", "bin", bin)
|
|
if fi, err := os.Stat(p); err == nil && !fi.IsDir() {
|
|
return p
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// LingerEnabled reports whether systemd lingering is enabled for the user,
|
|
// so user units start at boot without a login session.
|
|
func LingerEnabled(name string) bool {
|
|
_, err := os.Stat(filepath.Join("/var/lib/systemd/linger", name))
|
|
return err == nil
|
|
}
|