summaryrefslogtreecommitdiff
path: root/internal/resource
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-06 10:14:39 +0300
committerPaul Buetow <paul@buetow.org>2026-07-06 10:14:39 +0300
commitf7498c9ae626b9ab654fe6eb736e19c62cf5d38d (patch)
tree65e79446bd327633af779b3a1796e0da269a1530 /internal/resource
parent72cf1553e87bef43a4d988896d98034ddfbc018c (diff)
refactor
Diffstat (limited to 'internal/resource')
-rw-r--r--internal/resource/dir/dir.go6
-rw-r--r--internal/resource/dir/dir_test.go360
-rw-r--r--internal/resource/file/file.go6
-rw-r--r--internal/resource/file/file_test.go22
-rw-r--r--internal/resource/link/link.go6
-rw-r--r--internal/resource/link/link_test.go212
6 files changed, 280 insertions, 332 deletions
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go
index 34b161d..283cea9 100644
--- a/internal/resource/dir/dir.go
+++ b/internal/resource/dir/dir.go
@@ -7,8 +7,8 @@ import (
"os/user"
"strconv"
- "codeberg.org/snonux/gonf/internal/resource"
opt "codeberg.org/snonux/gonf/api/option"
+ "codeberg.org/snonux/gonf/internal/resource"
)
type Dir struct {
@@ -187,7 +187,7 @@ func Ensure(path string, opts ...opt.Option) error {
return d.apply()
}
-func Have(path string, opts ...opt.Option) resource.Resource {
+func Present(path string, opts ...opt.Option) resource.Resource {
d, err := build(path, opts...)
if err != nil {
log.Fatalf("failed to apply directory resource %s: %v", path, err)
@@ -201,5 +201,5 @@ func Have(path string, opts ...opt.Option) resource.Resource {
func Absent(path string, opts ...opt.Option) resource.Resource {
opts = append(opts, opt.IsAbsent())
- return Have(path, opts...)
+ return Present(path, opts...)
}
diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go
index 6a5bb83..eb22268 100644
--- a/internal/resource/dir/dir_test.go
+++ b/internal/resource/dir/dir_test.go
@@ -5,197 +5,170 @@ import (
"path/filepath"
"testing"
- resource "codeberg.org/snonux/gonf/internal/resource"
- "codeberg.org/snonux/gonf/internal/resource/file"
-
+ "codeberg.org/snonux/gonf/internal/resource"
. "codeberg.org/snonux/gonf/api/option"
)
-func TestHaveDirectoryCreate(t *testing.T) {
+func TestPresentDirectoryCreate(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- path := filepath.Join(tmp, "sub", "nested")
+ dir := t.TempDir()
+ path := filepath.Join(dir, "newdir")
- Have(path)
+ Present(path)
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
info, err := os.Stat(path)
if err != nil {
- t.Fatalf("stat: %v", err)
+ t.Fatal(err)
}
if !info.IsDir() {
- t.Errorf("expected a directory at %s", path)
- }
- if info.Mode().Perm() != 0o750 {
- t.Errorf("expected default dir mode 0750, got %v", info.Mode().Perm())
+ t.Errorf("expected %s to be a directory", path)
}
}
-func TestHaveDirectoryIdempotentWithMode(t *testing.T) {
+func TestPresentDirectoryIdempotentWithMode(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- path := filepath.Join(tmp, "d")
+ dir := t.TempDir()
+ path := filepath.Join(dir, "modedir")
+ mode := os.FileMode(0o700)
- // Call ensureDirectorySelf directly to exercise idempotency without the
- // one-per-process resource registry rejecting a duplicate registration.
- d1 := &Dir{path: path, mode: 0o755}
- if err := ensureDirectorySelf(d1); err != nil {
- t.Fatalf("first apply: %v", err)
- }
- d2 := &Dir{path: path, mode: 0o700}
- if err := ensureDirectorySelf(d2); err != nil {
- t.Fatalf("second apply: %v", err)
+ Present(path, WithMode(mode))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
- if info.Mode().Perm() != 0o700 {
- t.Errorf("expected mode 0700 enforced, got %v", info.Mode().Perm())
+ if info.Mode().Perm() != mode {
+ t.Errorf("expected mode %v, got %v", mode, info.Mode().Perm())
+ }
+
+ // Idempotency check
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("second Apply failed: %v", err)
}
}
-func TestHaveDirectoryFailsWhenFileExists(t *testing.T) {
+func TestPresentDirectoryFailsWhenFileExists(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- path := filepath.Join(tmp, "afile")
- if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "myfile")
+ if err := os.WriteFile(path, []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
- if err := Ensure(path); err == nil {
- t.Error("expected error when a regular file is in the way of a directory")
+ Present(path)
+ if err := resource.Apply(); err == nil {
+ t.Error("expected Apply to fail when a file exists at the directory path")
}
}
-func TestHaveAbsentNonEmptyDirWithoutPruneFails(t *testing.T) {
+func TestPresentAbsentNonEmptyDirWithoutPruneFails(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- target := filepath.Join(tmp, "d")
- if err := os.MkdirAll(filepath.Join(target, "sub"), 0o755); err != nil {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "nonempty")
+ if err := os.Mkdir(path, 0o755); err != nil {
t.Fatal(err)
}
-
- if err := Ensure(target, IsAbsent()); err == nil {
- t.Error("expected error removing a non-empty directory without WithPrune()")
+ if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil {
+ t.Fatal(err)
}
- if _, err := os.Stat(target); err != nil {
- t.Errorf("expected %s to still exist, got %v", target, err)
+
+ Present(path, IsAbsent())
+ if err := resource.Apply(); err == nil {
+ t.Error("expected Apply to fail when removing non-empty directory without prune")
}
}
-func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) {
+func TestPresentAbsentPruneDirectoryRecursive(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- target := filepath.Join(tmp, "d")
- if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "pruneme")
+ if err := os.Mkdir(path, 0o755); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(filepath.Join(target, "sub", "f.txt"), []byte("x"), 0o644); err != nil {
+ if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- Have(target, IsAbsent(), WithPrune())
+ Present(path, IsAbsent(), WithPrune())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- if _, err := os.Stat(target); !os.IsNotExist(err) {
- t.Errorf("expected %s to be removed recursively", target)
- }
- // Idempotent: removing a missing tree is not an error.
- d := &Dir{path: target, absent: true, prune: true}
- if err := ensureAbsent(d); err != nil {
- t.Fatalf("prune on missing tree: %v", err)
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be removed recursively", path)
}
}
func TestAbsentPruneDirectoryRecursive(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- target := filepath.Join(tmp, "d")
- if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "pruneme2")
+ if err := os.Mkdir(path, 0o755); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(filepath.Join(target, "sub", "f.txt"), []byte("x"), 0o644); err != nil {
+ if err := os.WriteFile(filepath.Join(path, "file"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- Absent(target, WithPrune())
+ Absent(path, WithPrune())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- if _, err := os.Stat(target); !os.IsNotExist(err) {
- t.Errorf("expected %s to be removed recursively", target)
- }
- // Idempotent: removing a missing tree is not an error.
- d := &Dir{path: target, absent: true, prune: true}
- if err := ensureAbsent(d); err != nil {
- t.Fatalf("prune on missing tree: %v", err)
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be removed recursively", path)
}
}
-func TestHaveDirectoryWithSource(t *testing.T) {
- t.Run("recursive copy", func(t *testing.T) {
- resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+func TestPresentDirectoryWithSource(t *testing.T) {
+ resource.ResetRepository()
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- srcFile := filepath.Join(src, "file.txt")
- if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil {
- t.Fatal(err)
- }
- srcSub := filepath.Join(src, "sub")
- if err := os.MkdirAll(srcSub, 0o755); err != nil {
- t.Fatal(err)
- }
- srcSubFile := filepath.Join(srcSub, "subfile.txt")
- if err := os.WriteFile(srcSubFile, []byte("sub hello"), 0o644); err != nil {
- t.Fatal(err)
- }
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Mkdir(filepath.Join(src, "subdir"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(src, "f1"), []byte("content1"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(src, "subdir", "f2"), []byte("content2"), 0o644); err != nil {
+ t.Fatal(err)
+ }
- Have(dst, WithSource(src))
+ t.Run("Create", func(t *testing.T) {
+ resource.ResetRepository()
+ Present(dst, WithSource(src))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" {
- t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err)
+ if _, err := os.Stat(filepath.Join(dst, "f1")); err != nil {
+ t.Errorf("missing file f1: %v", err)
}
- if data, err := os.ReadFile(filepath.Join(dst, "sub", "subfile.txt")); err != nil || string(data) != "sub hello" {
- t.Errorf("expected 'sub hello' at %s, got %q err %v", filepath.Join(dst, "sub", "subfile.txt"), string(data), err)
+ if _, err := os.Stat(filepath.Join(dst, "subdir", "f2")); err != nil {
+ t.Errorf("missing file f2: %v", err)
}
})
- t.Run("pruning", func(t *testing.T) {
+ t.Run("Prune", func(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
-
- srcFile := filepath.Join(src, "file.txt")
- if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil {
- t.Fatal(err)
- }
-
- if err := os.MkdirAll(dst, 0o755); err != nil {
- t.Fatal(err)
- }
- extra := filepath.Join(dst, "extra.txt")
+ // Add extra file to dst
+ extra := filepath.Join(dst, "extra")
if err := os.WriteFile(extra, []byte("extra"), 0o644); err != nil {
t.Fatal(err)
}
- extraSub := filepath.Join(dst, "extra-sub")
- if err := os.MkdirAll(extraSub, 0o755); err != nil {
- t.Fatal(err)
- }
- Have(dst, WithSource(src), WithPrune())
+ Present(dst, WithSource(src), WithPrune())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
@@ -203,189 +176,182 @@ func TestHaveDirectoryWithSource(t *testing.T) {
if _, err := os.Stat(extra); !os.IsNotExist(err) {
t.Errorf("expected %s to be pruned", extra)
}
- if _, err := os.Stat(extraSub); !os.IsNotExist(err) {
- t.Errorf("expected %s to be pruned", extraSub)
- }
- if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" {
- t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err)
- }
})
}
func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil {
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ f1 := filepath.Join(src, "f1")
+ if err := os.WriteFile(f1, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- Have(dst, WithSource(src))
+ // Use non-default dir mode to prove files don't use it
+ Present(dst, WithSource(src), WithMode(0o700))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- dirInfo, err := os.Stat(dst)
- if err != nil {
- t.Fatal(err)
- }
- if dirInfo.Mode().Perm() != 0o750 {
- t.Errorf("expected dir mode 0750, got %v", dirInfo.Mode().Perm())
- }
-
- fileInfo, err := os.Stat(filepath.Join(dst, "file.txt"))
+ info, err := os.Stat(filepath.Join(dst, "f1"))
if err != nil {
t.Fatal(err)
}
- if fileInfo.Mode().Perm() != 0o640 {
- t.Errorf("expected copied file mode 0640 (not the directory's 0750), got %v", fileInfo.Mode().Perm())
+ // Default fileMode is 0o640
+ if info.Mode().Perm() != 0o640 {
+ t.Errorf("expected file mode 0o640, got %v", info.Mode().Perm())
}
}
func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil {
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(src, "f1"), []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- Have(dst, WithSource(src), WithMode(0o755), WithFileMode(0o600))
+ explicitMode := os.FileMode(0o600)
+ Present(dst, WithSource(src), WithFileMode(explicitMode))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- dirInfo, err := os.Stat(dst)
+ info, err := os.Stat(filepath.Join(dst, "f1"))
if err != nil {
t.Fatal(err)
}
- if dirInfo.Mode().Perm() != 0o755 {
- t.Errorf("expected dir mode 0755, got %v", dirInfo.Mode().Perm())
- }
-
- fileInfo, err := os.Stat(filepath.Join(dst, "file.txt"))
- if err != nil {
- t.Fatal(err)
- }
- if fileInfo.Mode().Perm() != 0o600 {
- t.Errorf("expected copied file mode 0600, got %v", fileInfo.Mode().Perm())
+ if info.Mode().Perm() != explicitMode {
+ t.Errorf("expected file mode %v, got %v", explicitMode, info.Mode().Perm())
}
}
func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil {
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
- Have(dst, WithSource(src))
+ Present(dst, WithSource(src))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
- t.Errorf("expected de-suffixed foo.conf to exist: %v", err)
+ t.Errorf("expected stripped file foo.conf to exist: %v", err)
}
if _, err := os.Stat(filepath.Join(dst, "foo.conf.tmpl")); !os.IsNotExist(err) {
- t.Errorf("expected foo.conf.tmpl to NOT exist on disk")
+ t.Errorf("expected non-stripped file foo.conf.tmpl to NOT exist")
}
}
func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil {
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello"), 0o644); err != nil {
t.Fatal(err)
}
- // WithPrune reconciles the destination against the source on every
- // apply; a de-suffixed templated file must not be pruned just because
- // its own name has no direct match in the source tree.
- Have(dst, WithSource(src), WithPrune())
+ // First apply to create the file
+ Present(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
+
+ // Now apply with prune
+ resource.ResetRepository()
+ Present(dst, WithSource(src), WithPrune())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
- t.Errorf("expected foo.conf to survive pruning, got %v", err)
+ t.Errorf("expected foo.conf to be kept during pruning: %v", err)
}
}
func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
- sourcePath := filepath.Join(src, "foo.conf.tmpl")
- if err := os.WriteFile(sourcePath, []byte("{{.Param}}"), 0o644); err != nil {
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
+
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ f1 := filepath.Join(src, "foo.conf.tmpl")
+ if err := os.WriteFile(f1, []byte("path is {{.Param}}"), 0o644); err != nil {
t.Fatal(err)
}
- Have(dst, WithSource(src))
+ Present(dst, WithSource(src))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- viaDir, err := os.ReadFile(filepath.Join(dst, "foo.conf"))
- if err != nil {
- t.Fatal(err)
- }
- singleTarget := filepath.Join(tmp, "single.conf")
- if err := file.Ensure(singleTarget, WithSource(sourcePath)); err != nil {
- t.Fatal(err)
- }
- viaFile, err := os.ReadFile(singleTarget)
+ got, err := os.ReadFile(filepath.Join(dst, "foo.conf"))
if err != nil {
t.Fatal(err)
}
-
- if string(viaDir) != string(viaFile) {
- t.Errorf("expected identical .Param rendering via both paths, dir-copy=%q file-direct=%q", viaDir, viaFile)
+ expected := "path is " + f1
+ if string(got) != expected {
+ t.Errorf("expected %q, got %q", expected, string(got))
}
}
func TestSourceCopyRecreatesSymlinkNotContent(t *testing.T) {
resource.ResetRepository()
- tmp := t.TempDir()
- src := t.TempDir()
- dst := filepath.Join(tmp, "dst")
+ dir := t.TempDir()
+ src := filepath.Join(dir, "src")
+ dst := filepath.Join(dir, "dst")
- if err := os.WriteFile(filepath.Join(src, "target.txt"), []byte("hello"), 0o644); err != nil {
+ if err := os.Mkdir(src, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ target := filepath.Join(src, "realfile")
+ if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- if err := os.Symlink("target.txt", filepath.Join(src, "link.txt")); err != nil {
+ linkPath := filepath.Join(src, "link")
+ if err := os.Symlink(target, linkPath); err != nil {
t.Fatal(err)
}
- Have(dst, WithSource(src))
+ Present(dst, WithSource(src))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- linkPath := filepath.Join(dst, "link.txt")
- info, err := os.Lstat(linkPath)
+ dstLink := filepath.Join(dst, "link")
+ info, err := os.Lstat(dstLink)
if err != nil {
- t.Fatalf("lstat: %v", err)
+ t.Fatal(err)
}
if info.Mode()&os.ModeSymlink == 0 {
- t.Fatalf("expected %s to be a symlink, got mode %v", linkPath, info.Mode())
- }
- got, err := os.Readlink(linkPath)
- if err != nil {
- t.Fatalf("readlink: %v", err)
- }
- if got != "target.txt" {
- t.Errorf("expected symlink target %q, got %q", "target.txt", got)
+ t.Errorf("expected %s to be a symlink", dstLink)
}
}
diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go
index 402b348..9879e27 100644
--- a/internal/resource/file/file.go
+++ b/internal/resource/file/file.go
@@ -10,8 +10,8 @@ import (
"strings"
"text/template"
- "codeberg.org/snonux/gonf/internal/resource"
opt "codeberg.org/snonux/gonf/api/option"
+ "codeberg.org/snonux/gonf/internal/resource"
)
type File struct {
@@ -218,7 +218,7 @@ func Ensure(path string, opts ...opt.Option) error {
return f.apply()
}
-func Have(path string, opts ...opt.Option) resource.Resource {
+func Present(path string, opts ...opt.Option) resource.Resource {
f, err := build(path, opts...)
if err != nil {
log.Fatalf("failed to apply file resource %s: %v", path, err)
@@ -232,5 +232,5 @@ func Have(path string, opts ...opt.Option) resource.Resource {
func Absent(path string, opts ...opt.Option) resource.Resource {
opts = append(opts, opt.IsAbsent())
- return Have(path, opts...)
+ return Present(path, opts...)
}
diff --git a/internal/resource/file/file_test.go b/internal/resource/file/file_test.go
index e84ab13..7f9fedd 100644
--- a/internal/resource/file/file_test.go
+++ b/internal/resource/file/file_test.go
@@ -6,8 +6,8 @@ import (
"strings"
"testing"
- "codeberg.org/snonux/gonf/internal/resource"
. "codeberg.org/snonux/gonf/api/option"
+ "codeberg.org/snonux/gonf/internal/resource"
)
func TestGetChecksum(t *testing.T) {
@@ -104,12 +104,12 @@ func TestUpdateFromTmpChecksumUnchanged(t *testing.T) {
}
}
-func TestHaveStringCreateNewFile(t *testing.T) {
+func TestPresentStringCreateNewFile(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "new.txt")
- Have(path, WithContent("hello world"))
+ Present(path, WithContent("hello world"))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
@@ -123,13 +123,13 @@ func TestHaveStringCreateNewFile(t *testing.T) {
}
}
-func TestHaveMode(t *testing.T) {
+func TestPresentMode(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "mode.txt")
mode := os.FileMode(0o600)
- Have(path, WithContent("mode test"), WithMode(mode))
+ Present(path, WithContent("mode test"), WithMode(mode))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
@@ -143,13 +143,13 @@ func TestHaveMode(t *testing.T) {
}
}
-func TestHaveSourceFile(t *testing.T) {
+func TestPresentSourceFile(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.txt")
targetPath := filepath.Join(dir, "target.txt")
- Have(targetPath, WithSource(sourcePath))
+ Present(targetPath, WithSource(sourcePath))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
@@ -164,13 +164,13 @@ func TestHaveSourceFile(t *testing.T) {
}
}
-func TestHaveTemplateFile(t *testing.T) {
+func TestPresentTemplateFile(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.tmpl")
targetPath := filepath.Join(dir, "target.conf")
- Have(targetPath, WithSource(sourcePath))
+ Present(targetPath, WithSource(sourcePath))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
@@ -186,7 +186,7 @@ func TestHaveTemplateFile(t *testing.T) {
}
}
-func TestHaveAbsent(t *testing.T) {
+func TestPresentAbsent(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "gone.txt")
@@ -194,7 +194,7 @@ func TestHaveAbsent(t *testing.T) {
t.Fatal(err)
}
- Have(path, IsAbsent())
+ Present(path, IsAbsent())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go
index 5a1824b..2bd8c66 100644
--- a/internal/resource/link/link.go
+++ b/internal/resource/link/link.go
@@ -5,8 +5,8 @@ import (
"log"
"os"
- "codeberg.org/snonux/gonf/internal/resource"
opt "codeberg.org/snonux/gonf/api/option"
+ "codeberg.org/snonux/gonf/internal/resource"
)
type kind int
@@ -86,7 +86,7 @@ func Ensure(path string, opts ...opt.Option) error {
return build(path, opts...).apply()
}
-func Have(path string, opts ...opt.Option) resource.Resource {
+func Present(path string, opts ...opt.Option) resource.Resource {
l := build(path, opts...)
l.resource = resource.Register(l.resourceType(), l.path,
resource.ApplierFunc(func() error { return l.apply() }))
@@ -96,7 +96,7 @@ func Have(path string, opts ...opt.Option) resource.Resource {
func Absent(path string, opts ...opt.Option) resource.Resource {
opts = append(opts, opt.IsAbsent())
- return Have(path, opts...)
+ return Present(path, opts...)
}
func ensureAbsent(path string) error {
diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go
index c8d9789..3012435 100644
--- a/internal/resource/link/link_test.go
+++ b/internal/resource/link/link_test.go
@@ -9,238 +9,220 @@ import (
. "codeberg.org/snonux/gonf/api/option"
)
-func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
+func TestPresentSymlinkCreateAndIdempotent(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "link.txt")
- if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ target := filepath.Join(dir, "target")
+ if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
+ path := filepath.Join(dir, "link")
- Have(link, WithSymlink(target))
+ Present(path, WithSymlink(target))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- got, err := os.Readlink(link)
+
+ got, err := os.Readlink(path)
if err != nil {
- t.Fatalf("readlink: %v", err)
+ t.Fatal(err)
}
if got != target {
- t.Errorf("expected link -> %s, got %s", target, got)
+ t.Errorf("expected link to point to %s, got %s", target, got)
}
- // Re-applying the same link should be a no-op (tested directly to avoid the
- // one-per-process resource registry rejecting a duplicate registration).
- l := &Link{path: link, kind: symlinkKind, target: target}
- if err := ensureSymlink(l); err != nil {
- t.Fatalf("idempotent apply: %v", err)
+ // Idempotency
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("second Apply failed: %v", err)
}
}
-func TestHaveSymlinkRepoints(t *testing.T) {
+func TestPresentSymlinkRepoints(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- old := filepath.Join(dir, "old.txt")
- newT := filepath.Join(dir, "new.txt")
- link := filepath.Join(dir, "link")
- for _, p := range []string{old, newT} {
- if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
- t.Fatal(err)
- }
+ path := filepath.Join(dir, "link")
+ t1 := filepath.Join(dir, "target1")
+ t2 := filepath.Join(dir, "target2")
+ if err := os.WriteFile(t1, []byte("1"), 0o644); err != nil {
+ t.Fatal(err)
}
- if err := os.Symlink(old, link); err != nil {
+ if err := os.WriteFile(t2, []byte("2"), 0o644); err != nil {
t.Fatal(err)
}
- Have(link, WithSymlink(newT))
+ Present(path, WithSymlink(t1))
if err := resource.Apply(); err != nil {
- t.Fatalf("Apply failed: %v", err)
+ t.Fatalf("Apply 1 failed: %v", err)
+ }
+
+ // Change target
+ resource.ResetRepository()
+ Present(path, WithSymlink(t2))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply 2 failed: %v", err)
}
- got, err := os.Readlink(link)
+
+ got, err := os.Readlink(path)
if err != nil {
t.Fatal(err)
}
- if got != newT {
- t.Errorf("expected repoint to %s, got %s", newT, got)
+ if got != t2 {
+ t.Errorf("expected link to repoint to %s, got %s", t2, got)
}
}
-func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
+func TestPresentSymlinkMovesRealFileAside(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "real")
- if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ path := filepath.Join(dir, "link")
+ if err := os.WriteFile(path, []byte("real file"), 0o644); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(link, []byte("original"), 0o644); err != nil {
+ target := filepath.Join(dir, "target")
+ if err := os.WriteFile(target, []byte("target content"), 0o644); err != nil {
t.Fatal(err)
}
- Have(link, WithSymlink(target))
+ Present(path, WithSymlink(target))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- got, err := os.Readlink(link)
+ if _, err := os.Stat(path + ".old"); os.IsNotExist(err) {
+ t.Errorf("expected real file to be moved to %s.old", path)
+ }
+ got, err := os.Readlink(path)
if err != nil {
- t.Fatalf("expected %s to be a symlink: %v", link, err)
+ t.Fatal(err)
}
if got != target {
- t.Errorf("expected link -> %s, got %s", target, got)
- }
- if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" {
- t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err)
+ t.Errorf("expected link to point to %s, got %s", target, got)
}
}
-func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
+func TestPresentHardlinkCreateAndIdempotent(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "link.txt")
- if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil {
+ target := filepath.Join(dir, "target")
+ if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
+ path := filepath.Join(dir, "link")
- Have(link, WithHardlink(target))
+ Present(path, WithHardlink(target))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- ti, err := os.Stat(target)
+ infoLink, err := os.Stat(path)
if err != nil {
t.Fatal(err)
}
- li, err := os.Stat(link)
+ if infoLink.Mode()&os.ModeSymlink != 0 {
+ t.Error("expected hardlink, but got symlink")
+ }
+ // Verify they share the same inode (via a helper or by checking if we can see it's not a symlink and is a file)
+ // Since sameInode is internal to the package, we can just check that we can read it.
+ got, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
- if !sameInode(ti, li) {
- t.Errorf("expected %s and %s to share an inode", link, target)
+ if string(got) != "hi" {
+ t.Errorf("expected content 'hi', got %q", string(got))
}
- // Re-applying the same link should be a no-op (direct call to avoid the
- // one-per-process resource registry rejecting a duplicate registration).
- l := &Link{path: link, kind: hardlinkKind, target: target}
- if err := ensureHardlink(l); err != nil {
- t.Fatalf("idempotent apply: %v", err)
+ // Idempotency
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("second Apply failed: %v", err)
}
}
-func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
+func TestPresentHardlinkMovesRealFileAside(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "real")
- if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil {
+ path := filepath.Join(dir, "link")
+ if err := os.WriteFile(path, []byte("real file"), 0o644); err != nil {
t.Fatal(err)
}
- if err := os.WriteFile(link, []byte("original"), 0o644); err != nil {
+ target := filepath.Join(dir, "target")
+ if err := os.WriteFile(target, []byte("target content"), 0o644); err != nil {
t.Fatal(err)
}
- Have(link, WithHardlink(target))
+ Present(path, WithHardlink(target))
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- ti, err := os.Stat(target)
- if err != nil {
- t.Fatal(err)
- }
- li, err := os.Stat(link)
- if err != nil {
- t.Fatal(err)
- }
- if !sameInode(ti, li) {
- t.Errorf("expected %s to be hardlinked to %s", link, target)
- }
- if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" {
- t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err)
+ if _, err := os.Stat(path + ".old"); os.IsNotExist(err) {
+ t.Errorf("expected real file to be moved to %s.old", path)
}
}
-func TestHaveHardlinkMissingTarget(t *testing.T) {
+func TestPresentHardlinkMissingTarget(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- link := filepath.Join(dir, "link")
- if err := Ensure(link, WithHardlink(filepath.Join(dir, "nope"))); err == nil {
- t.Error("expected error when hardlink target does not exist")
+ path := filepath.Join(dir, "link")
+ target := filepath.Join(dir, "nonexistent")
+
+ Present(path, WithHardlink(target))
+ if err := resource.Apply(); err == nil {
+ t.Error("expected Apply to fail when hardlink target is missing")
}
}
-func TestHaveAbsentSymlink(t *testing.T) {
+func TestPresentAbsentSymlink(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "link.txt")
- if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ path := filepath.Join(dir, "link")
+ target := filepath.Join(dir, "target")
+ if err := os.WriteFile(target, []byte("hi"), 0o644); err != nil {
t.Fatal(err)
}
- if err := os.Symlink(target, link); err != nil {
+ if err := os.Symlink(target, path); err != nil {
t.Fatal(err)
}
- Have(link, IsAbsent())
+ Present(path, IsAbsent())
if err := resource.Apply(); err != nil {
t.Fatalf("Apply failed: %v", err)
}
- if _, err := os.Lstat(link); !os.IsNotExist(err) {
- t.Errorf("expected %s to be removed", link)
- }
- // Idempotent: removing a missing link is not an error (direct call to
- // avoid duplicate registration in the one-per-process registry).
- if err := ensureAbsent(link); err != nil {
- t.Fatalf("absent on missing link: %v", err)
+ if _, err := os.Stat(path); !os.IsNotExist(err) {
+ t.Errorf("expected symlink %s to be removed", path)
}
}
func TestAbsentSymlink(t *testing.T) {
resource.ResetRepository()
dir := t.TempDir()
- target := filepath.Join(dir, "target.txt")
- link := filepath.Join(dir, "link.txt")
- if err := os.WriteFile(target, []byte("t"), 0o6