From ab6e1341dc5eb06b6573571f8c17e19d0c393418 Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Tue, 7 Jul 2026 00:17:27 +0300 Subject: can manage dnf packages --- Magefile.go | 9 ++++++- api/api.go | 13 +++++++--- api/options/option.go | 12 +++++++++ internal/resource/pkg/dnf.go | 20 +++++++-------- internal/resource/pkg/dnf_test.go | 52 +++++++++++++++++++++++++++++++++++++ internal/resource/pkg/pkg.go | 54 +++++++++++++++++++++++++++------------ 6 files changed, 129 insertions(+), 31 deletions(-) create mode 100644 internal/resource/pkg/dnf_test.go diff --git a/Magefile.go b/Magefile.go index 0c3c59c..670bb1c 100644 --- a/Magefile.go +++ b/Magefile.go @@ -44,7 +44,14 @@ func Run() error { // Test runs all unit tests. func Test() error { fmt.Println("testing...") - return run("go", "test", "-v", "./...") + return run("go", "test", "-v", "-count=1", "./...") +} + +// TestDNF runs DNF-specific integration tests. This requires root privileges. +func TestDNF() error { + fmt.Println("testing DNF integration...") + // Use 'env' to set the variable for the go test command + return run("env", "GONF_RUN_DNF_TESTS=1", "go", "test", "-v", "-count=1", "./internal/resource/pkg/...") } // Lint runs go vet. diff --git a/api/api.go b/api/api.go index 6e25151..043b693 100644 --- a/api/api.go +++ b/api/api.go @@ -5,6 +5,7 @@ import ( "codeberg.org/snonux/gonf/internal/resource/dir" "codeberg.org/snonux/gonf/internal/resource/file" "codeberg.org/snonux/gonf/internal/resource/link" + "codeberg.org/snonux/gonf/internal/resource/pkg" ) // File creates a file resource. @@ -37,6 +38,12 @@ func NoLink(path string, opts ...options.Option) Resource { return link.Absent(path, opts...) } -// func Package(name string, ...options.Options) Resource { -// return pkg.Present(name, opts...) -// } +// Package creates a package resource. +func Package(name string, opts ...options.Option) Resource { + return pkg.Present(name, opts...) +} + +// NoPackage creates a package resource that is ensured to be absent. +func NoPackage(name string, opts ...options.Option) Resource { + return pkg.Absent(name, opts...) +} diff --git a/api/options/option.go b/api/options/option.go index fcffccd..1d9b931 100644 --- a/api/options/option.go +++ b/api/options/option.go @@ -21,6 +21,7 @@ type ( FileModed interface{ SetFileMode(os.FileMode) } Prunable interface{ SetPrune() } Absentable interface{ SetAbsent() } + Latestable interface{ SetLatest() } Linkable interface { SetSymlink(target string) SetHardlink(target string) @@ -117,6 +118,17 @@ var IsAbsent = func(t any) { func IsAbsentFunc() Option { return IsAbsent } +// IsLatest marks the package resource to be updated to the latest version. +var IsLatest = func(t any) { + r, ok := t.(Latestable) + if !ok { + log.Fatalf("%T does not support IsLatest", t) + } + r.SetLatest() +} + +func IsLatestFunc() Option { return IsLatest } + // WithSymlink makes the resource a symbolic link pointing at target. func WithSymlink(target string) Option { return func(t any) { diff --git a/internal/resource/pkg/dnf.go b/internal/resource/pkg/dnf.go index ad03da2..e970b10 100644 --- a/internal/resource/pkg/dnf.go +++ b/internal/resource/pkg/dnf.go @@ -2,23 +2,21 @@ package pkg import ( "fmt" - "log" "codeberg.org/snonux/gonf/internal/exec" ) -func applyDNF(name string, ensure Ensure) error { +func applyDNF(p *Package) error { var args []string - switch ensure { - case PkgPresent: - args = []string{"install", "-y", name} - case PkgAbsent: - args = []string{"remove", "-y", name} - case PkgLatest: - args = []string{"install", "-y", name} - default: - log.Fatalf("unsupported ensure state: %v", ensure) + if p.absent { + args = []string{"remove", "-y", p.name} + } else if p.latest { + // update ensures the package is installed and updated to the latest version. + args = []string{"update", "-y", p.name} + } else { + // install ensures the package is installed, but does not update it if already present. + args = []string{"install", "-y", p.name} } stdout, stderr, exitCode, err := exec.Run("dnf", args...) diff --git a/internal/resource/pkg/dnf_test.go b/internal/resource/pkg/dnf_test.go new file mode 100644 index 0000000..e9778dd --- /dev/null +++ b/internal/resource/pkg/dnf_test.go @@ -0,0 +1,52 @@ +package pkg + +import ( + "os" + "testing" +) + +func TestApplyDNF(t *testing.T) { + // Only run this test if explicitly enabled via environment variable. + if os.Getenv("GONF_RUN_DNF_TESTS") != "1" { + t.Skip("Skipping DNF test: GONF_RUN_DNF_TESTS=1 not set") + } + + // Skip if not running as root, as dnf requires superuser privileges. + if os.Getuid() != 0 { + t.Skip("Skipping DNF test: root privileges required") + } + + p := &Package{ + name: "tig", + } + + t.Run("Present", func(t *testing.T) { + p.absent = false + p.latest = false + if err := applyDNF(p); err != nil { + t.Errorf("applyDNF Present failed: %v", err) + } + }) + + t.Run("Latest", func(t *testing.T) { + p.absent = false + p.latest = true + if err := applyDNF(p); err != nil { + t.Errorf("applyDNF Latest failed: %v", err) + } + }) + + t.Run("Absent", func(t *testing.T) { + p.absent = true + p.latest = false + if err := applyDNF(p); err != nil { + t.Errorf("applyDNF Absent failed: %v", err) + } + + // // Restore the package so we don't leave the system in a changed state + // p.absent = false + // if err := applyDNF(p); err != nil { + // t.Errorf("failed to restore package tig after Absent test: %v", err) + // } + }) +} diff --git a/internal/resource/pkg/pkg.go b/internal/resource/pkg/pkg.go index 72f098c..b010c7a 100644 --- a/internal/resource/pkg/pkg.go +++ b/internal/resource/pkg/pkg.go @@ -3,38 +3,60 @@ package pkg import ( "errors" "os" -) - -type Ensure int -const ( - PkgPresent Ensure = iota - PkgAbsent - PkgLatest + opt "codeberg.org/snonux/gonf/api/options" + "codeberg.org/snonux/gonf/internal/resource" ) -type applyFunc func(name string, ensure Ensure) error +type Package struct { + name string + absent bool + latest bool +} + +func (p *Package) SetAbsent() { p.absent = true } +func (p *Package) SetLatest() { p.latest = true } -func Present(name string, ensure Ensure) error { - bin, err := detect() +func (p *Package) apply() error { + pkgMan, err := detectPackageManager() if err != nil { return err } - var applyFunc applyFunc - - switch bin { + switch pkgMan { case "dnf": - applyFunc = applyDNF + return applyDNF(p) } - return applyFunc(name, ensure) + return errors.New("unsupported package manager") } -func detect() (string, error) { +func Present(name string, opts ...opt.Option) resource.Resource { + p := &Package{ + name: name, + } + + for _, o := range opts { + o(p) + } + + return resource.Register("Package", p.name, + resource.ApplierFunc(func() error { return p.apply() })) +} + +func Absent(name string, opts ...opt.Option) resource.Resource { + opts = append(opts, opt.IsAbsent) + return Present(name, opts...) +} + +func detectPackageManager() (string, error) { switch { case exists("/etc/fedora-release"): fallthrough + case exists("/etc/centos-release"): + fallthrough + case exists("/etc/redhat-release"): + fallthrough case exists("/etc/rocky-release"): return "dnf", nil } -- cgit v1.2.3