Files
unitdore/sysuser/sysuser.go
T
warkanum f0827fcdd7 feat: podman-compose stack units and root/user execution scopes
- 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
2026-10-12 19:05:51 +02:00

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
}