summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-07 00:17:27 +0300
committerPaul Buetow <paul@buetow.org>2026-07-07 00:17:27 +0300
commitab6e1341dc5eb06b6573571f8c17e19d0c393418 (patch)
treef9aa00bf0cf3ab302ecb2d152f7e781793112ba5
parent62ff4e782bc057d277a7c41fa35a665e2aeca045 (diff)
can manage dnf packages
-rw-r--r--Magefile.go9
-rw-r--r--api/api.go13
-rw-r--r--api/options/option.go12
-rw-r--r--internal/resource/pkg/dnf.go20
-rw-r--r--internal/resource/pkg/dnf_test.go52
-rw-r--r--internal/resource/pkg/pkg.go54
6 files changed, 129 insertions, 31 deletions
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
}