summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-09 00:14:33 +0300
committerPaul Buetow <paul@buetow.org>2026-07-09 00:14:33 +0300
commit756ae621bd97a8b776fefb15e5e7e2292f98ee45 (patch)
treeb454f958e6aef70bf7ca40dcbc106323cee2a42f
parent3f8b79f2b1384e3abc2b1bb2fe913688c7ec251c (diff)
dependencies
-rw-r--r--AGENTS.md18
-rw-r--r--api/options/option.go21
-rw-r--r--api/resource.go4
-rw-r--r--examples/examples.go47
-rw-r--r--internal/resource/dependencies_test.go75
-rw-r--r--internal/resource/dir/dir.go11
-rw-r--r--internal/resource/embed/embed.go24
-rw-r--r--internal/resource/file/file.go11
-rw-r--r--internal/resource/link/link.go11
-rw-r--r--internal/resource/multi.go20
-rw-r--r--internal/resource/pkg/dnf.go2
-rw-r--r--internal/resource/pkg/dnf_test.go26
-rw-r--r--internal/resource/pkg/pkg.go7
-rw-r--r--internal/resource/repository.go28
-rw-r--r--internal/resource/repository_test.go47
-rw-r--r--internal/resource/resource.go27
16 files changed, 326 insertions, 53 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 3d3cd6d..338280b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,3 +2,21 @@
## Resource Management
All public functions which start with `Have` should be responsible for registering the resource they create.
+
+## Dependencies
+Concrete resource types (e.g. `file.File`, `dir.Dir`, `link.Link`, `pkg.Package`)
+embed `embed.DependsOn` to accumulate dependency IDs. The `DependsOn` option
+records those IDs, and each `Present` function forwards them into
+`resource.Register(..., x.DependsOn.IDs...)`. A `Multi` dependency is expanded
+via `Dependencies()` so each member is depended upon individually, and
+`repository.apply()` topologically sorts resources by their `dependsOn` set.
+
+## Shared embeds
+State common to all concrete resource types lives in the `embed` package and is
+embedded rather than redeclared:
+- `embed.DependsOn` — dependency IDs (`IDs` field) plus the `AddDependency` method.
+- `embed.Absence` — the `Absent` field plus the `SetAbsent` method (implements
+ `opt.Absentable`).
+
+When adding a field or capability shared by every resource type, prefer a new
+embed type here instead of duplicating the field and its setter in each resource.
diff --git a/api/options/option.go b/api/options/option.go
index 1d9b931..22156a8 100644
--- a/api/options/option.go
+++ b/api/options/option.go
@@ -5,6 +5,8 @@ package options
import (
"log"
"os"
+
+ "codeberg.org/snonux/gonf/internal/resource"
)
// Option configures a resource. It is applied to the concrete resource value
@@ -22,12 +24,31 @@ type (
Prunable interface{ SetPrune() }
Absentable interface{ SetAbsent() }
Latestable interface{ SetLatest() }
+ Dependable interface{ AddDependency(id string) }
Linkable interface {
SetSymlink(target string)
SetHardlink(target string)
}
)
+// DependsOn declares that the resource being configured must be applied only
+// after every given resource has been applied. Each argument may be a single
+// resource or a Multi; a Multi is expanded so the dependency is recorded for
+// each of its members individually.
+func DependsOn(deps ...resource.Dependency) Option {
+ return func(t any) {
+ r, ok := t.(Dependable)
+ if !ok {
+ log.Fatalf("%T does not support DependsOn", t)
+ }
+ for _, dep := range deps {
+ for _, id := range dep.Dependencies() {
+ r.AddDependency(id)
+ }
+ }
+ }
+}
+
// WithOwner sets the owning user of the resource.
func WithOwner(owner string) Option {
return func(t any) {
diff --git a/api/resource.go b/api/resource.go
index e254e1b..5c95211 100644
--- a/api/resource.go
+++ b/api/resource.go
@@ -10,6 +10,10 @@ import (
type Resource interface {
ID() string
String() string
+ // Dependencies returns the flattened resource IDs this value represents,
+ // so it can be passed to the DependsOn option. A single resource yields
+ // its own ID; a Multi yields the IDs of all its members.
+ Dependencies() []string
}
func Apply() error {
diff --git a/examples/examples.go b/examples/examples.go
index 7bbfa46..dee9984 100644
--- a/examples/examples.go
+++ b/examples/examples.go
@@ -67,10 +67,10 @@ func Run() error {
_ = os.Link("/tmp/gonf_hello.txt", "/tmp/gonf_stale_hardlink")
Link("/tmp/gonf_stale_hardlink", IsAbsent)
- // 13. Package management
- Package("tig") // Ensure installed (Present)
- Package("vim", IsLatest) // Ensure installed and latest version
- NoPackage("nano") // Ensure absent
+ // // 13. Package management
+ // Package("tig") // Ensure installed (Present)
+ // Package("vim", IsLatest) // Ensure installed and latest version
+ // NoPackage("nano") // Ensure absent
// 14. Multi-resource declarations
// Create multiple files with the same options
@@ -86,17 +86,17 @@ func Run() error {
), WithMode(0o755))
// Install multiple packages and ensure they are latest
- Package(Elems(
- "htop",
- "curl",
- "wget",
- ), IsLatest)
-
- // Remove multiple packages
- NoPackage(Elems(
- "old-pkg1",
- "old-pkg2",
- ))
+ // Package(Elems(
+ // "htop",
+ // "curl",
+ // "wget",
+ // ), IsLatest)
+
+ // // Remove multiple packages
+ // NoPackage(Elems(
+ // "old-pkg1",
+ // "old-pkg2",
+ // ))
// Remove multiple files
NoFile(Elems(
@@ -104,5 +104,22 @@ func Run() error {
"/tmp/stale2.txt",
))
+ // 15. Ordering with DependsOn. bar.txt is only applied after foo.txt.
+ fooRes := File("/tmp/gonf_foo.txt", WithContent("foo"))
+ File("/tmp/gonf_bar.txt", WithContent("bar"), DependsOn(fooRes))
+
+ // DependsOn also accepts multi-resources; the dependent then waits for
+ // every individual member of the multi.
+ multiRes := File(Elems(
+ "/tmp/gonf_dep1.txt",
+ "/tmp/gonf_dep2.txt",
+ ), WithContent("dep"))
+ File("/tmp/gonf_after_multi.txt", WithContent("after"), DependsOn(multiRes))
+
+ // DependsOn works across resource types and accepts several resources at
+ // once: this directory is created only after both the file and the multi
+ // above have been applied.
+ Dir("/tmp/gonf_after_dir", DependsOn(fooRes, multiRes))
+
return Apply()
}
diff --git a/internal/resource/dependencies_test.go b/internal/resource/dependencies_test.go
new file mode 100644
index 0000000..e298740
--- /dev/null
+++ b/internal/resource/dependencies_test.go
@@ -0,0 +1,75 @@
+package resource
+
+import (
+ "reflect"
+ "testing"
+)
+
+func TestResourceDependencies(t *testing.T) {
+ r := Resource{Type: "File", Name: "/tmp/a"}
+
+ got := r.Dependencies()
+ want := []string{"File[/tmp/a]"}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Resource.Dependencies() = %v, want %v", got, want)
+ }
+}
+
+// TestMultiDependencies ensures a Multi flattens into each member's individual
+// ID, so depending on a Multi records a dependency on every member.
+func TestMultiDependencies(t *testing.T) {
+ m := Multi{
+ Resource{Type: "File", Name: "/tmp/a"},
+ Resource{Type: "File", Name: "/tmp/b"},
+ }
+
+ got := m.Dependencies()
+ want := []string{"File[/tmp/a]", "File[/tmp/b]"}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("Multi.Dependencies() = %v, want %v", got, want)
+ }
+}
+
+// TestRegisterWithDeps ensures Register seeds dependsOn from the variadic deps
+// and de-duplicates repeated IDs.
+func TestRegisterWithDeps(t *testing.T) {
+ ResetRepository()
+
+ res := Register("File", "/tmp/a", &mockApplier{}, "File[b]", "File[c]", "File[b]")
+
+ if _, ok := res.dependsOn["File[b]"]; !ok {
+ t.Error("expected dependency File[b]")
+ }
+ if _, ok := res.dependsOn["File[c]"]; !ok {
+ t.Error("expected dependency File[c]")
+ }
+ if len(res.dependsOn) != 2 {
+ t.Errorf("expected 2 unique dependencies, got %d: %v", len(res.dependsOn), res.dependsOn)
+ }
+}
+
+func TestRegisterNoDeps(t *testing.T) {
+ ResetRepository()
+
+ res := Register("File", "/tmp/a", &mockApplier{})
+ if res.dependsOn == nil {
+ t.Error("expected dependsOn to be initialized, got nil")
+ }
+ if len(res.dependsOn) != 0 {
+ t.Errorf("expected no dependencies, got %v", res.dependsOn)
+ }
+}
+
+// TestSortedDependsOn ensures dependency IDs are returned sorted for stable log
+// output regardless of insertion/map order.
+func TestSortedDependsOn(t *testing.T) {
+ ResetRepository()
+
+ res := Register("File", "/tmp/a", &mockApplier{}, "File[c]", "File[a]", "File[b]")
+
+ got := res.sortedDependsOn()
+ want := []string{"File[a]", "File[b]", "File[c]"}
+ if !reflect.DeepEqual(got, want) {
+ t.Errorf("sortedDependsOn() = %v, want %v", got, want)
+ }
+}
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go
index 83c017c..5506dbb 100644
--- a/internal/resource/dir/dir.go
+++ b/internal/resource/dir/dir.go
@@ -9,9 +9,12 @@ import (
opt "codeberg.org/snonux/gonf/api/options"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/embed"
)
type Dir struct {
+ embed.DependsOn
+ embed.Absence
resource resource.Resource
path string
source string
@@ -20,7 +23,6 @@ type Dir struct {
mode os.FileMode // this directory's own mode, default 0o750
fileMode os.FileMode // mode for regular files copied from source, default 0o640
prune bool // reconciles extra dest files during a source copy, and recursive-remove during IsAbsent()
- absent bool
}
// SetSource implements opt.Sourced.
@@ -41,9 +43,6 @@ func (d *Dir) SetFileMode(mode os.FileMode) { d.fileMode = mode }
// SetPrune implements opt.Prunable.
func (d *Dir) SetPrune() { d.prune = true }
-// SetAbsent implements opt.Absentable.
-func (d *Dir) SetAbsent() { d.absent = true }
-
func build(path string, opts ...opt.Option) (*Dir, error) {
curr, err := user.Current()
if err != nil {
@@ -68,7 +67,7 @@ func build(path string, opts ...opt.Option) (*Dir, error) {
// apply performs the idempotent OS work for d without registering a
// resource.
func (d *Dir) apply() error {
- if d.absent {
+ if d.Absent {
return ensureAbsent(d)
}
@@ -194,7 +193,7 @@ func Present(path string, opts ...opt.Option) resource.Resource {
}
d.resource = resource.Register("Directory", d.path,
- resource.ApplierFunc(func() error { return d.apply() }))
+ resource.ApplierFunc(func() error { return d.apply() }), d.DependsOn.IDs...)
return d.resource
}
diff --git a/internal/resource/embed/embed.go b/internal/resource/embed/embed.go
new file mode 100644
index 0000000..49fe3eb
--- /dev/null
+++ b/internal/resource/embed/embed.go
@@ -0,0 +1,24 @@
+package embed
+
+// DependsOn is embedded into concrete resource types to give them the ability
+// to accumulate dependency IDs supplied via the DependsOn option.
+type DependsOn struct {
+ IDs []string
+}
+
+// AddDependency records a single resource ID this resource depends on. It uses
+// a pointer receiver so the mutation is visible to the embedding value.
+func (d *DependsOn) AddDependency(id string) {
+ d.IDs = append(d.IDs, id)
+}
+
+// Absence is embedded into concrete resource types that can be marked for
+// removal via the IsAbsent option. It promotes an Absent field and a
+// SetAbsent method to the embedding type.
+type Absence struct {
+ Absent bool
+}
+
+// SetAbsent implements opt.Absentable, marking the resource for removal. It
+// uses a pointer receiver so the mutation is visible to the embedding value.
+func (a *Absence) SetAbsent() { a.Absent = true }
diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go
index 88b0bca..2dc1df5 100644
--- a/internal/resource/file/file.go
+++ b/internal/resource/file/file.go
@@ -12,9 +12,12 @@ import (
opt "codeberg.org/snonux/gonf/api/options"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/embed"
)
type File struct {
+ embed.DependsOn
+ embed.Absence
resource resource.Resource
path string
content string
@@ -22,7 +25,6 @@ type File struct {
user string
group string
mode os.FileMode
- absent bool
}
// SetContent implements opt.Contented. Setting literal content clears any
@@ -48,9 +50,6 @@ func (f *File) SetGroup(group string) { f.group = group }
// SetMode implements opt.Moded.
func (f *File) SetMode(mode os.FileMode) { f.mode = mode }
-// SetAbsent implements opt.Absentable.
-func (f *File) SetAbsent() { f.absent = true }
-
func build(path string, opts ...opt.Option) (*File, error) {
curr, err := user.Current()
if err != nil {
@@ -74,7 +73,7 @@ func build(path string, opts ...opt.Option) (*File, error) {
// apply performs the idempotent OS work for f without registering a
// resource.
func (f *File) apply() error {
- if f.absent {
+ if f.Absent {
return ensureAbsent(f.targetPath())
}
@@ -225,7 +224,7 @@ func Present(path string, opts ...opt.Option) resource.Resource {
}
f.resource = resource.Register("File", f.targetPath(),
- resource.ApplierFunc(func() error { return f.apply() }))
+ resource.ApplierFunc(func() error { return f.apply() }), f.DependsOn.IDs...)
return f.resource
}
diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go
index b40ce65..836a925 100644
--- a/internal/resource/link/link.go
+++ b/internal/resource/link/link.go
@@ -7,6 +7,7 @@ import (
opt "codeberg.org/snonux/gonf/api/options"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/embed"
)
type kind int
@@ -18,11 +19,12 @@ const (
)
type Link struct {
+ embed.DependsOn
+ embed.Absence
resource resource.Resource
path string
target string
kind kind
- absent bool
}
// SetSymlink implements opt.Linkable.
@@ -37,9 +39,6 @@ func (l *Link) SetHardlink(target string) {
l.target = target
}
-// SetAbsent implements opt.Absentable.
-func (l *Link) SetAbsent() { l.absent = true }
-
func build(path string, opts ...opt.Option) *Link {
l := &Link{path: path}
for _, o := range opts {
@@ -54,7 +53,7 @@ func build(path string, opts ...opt.Option) *Link {
// was validated.
func (l *Link) apply() error {
switch {
- case l.absent:
+ case l.Absent:
return ensureAbsent(l.path)
case l.kind == symlinkKind:
return ensureSymlink(l)
@@ -89,7 +88,7 @@ func Ensure(path string, opts ...opt.Option) error {
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() }))
+ resource.ApplierFunc(func() error { return l.apply() }), l.DependsOn.IDs...)
return l.resource
}
diff --git a/internal/resource/multi.go b/internal/resource/multi.go
index 1694814..3916287 100644
--- a/internal/resource/multi.go
+++ b/internal/resource/multi.go
@@ -5,6 +5,13 @@ import (
"strings"
)
+// Dependency is implemented by anything that can be depended upon. It returns
+// the flattened list of individual resource IDs, so that depending on a Multi
+// expands into a dependency on each of its members individually.
+type Dependency interface {
+ Dependencies() []string
+}
+
// Multi is a collection of resources that satisfies the api.Resource interface.
type Multi []Resource
@@ -28,6 +35,19 @@ func (m Multi) ID() string {
return strings.Join(ids, "+")
}
+// Dependencies flattens the Multi into the IDs of each of its members, so a
+// dependency on a Multi becomes an individual dependency on every resource it
+// contains.
+func (m Multi) Dependencies() []string {
+ ids := make([]string, 0, len(m))
+
+ for _, res := range m {
+ ids = append(ids, res.Dependencies()...)
+ }
+
+ return ids
+}
+
func (m Multi) Apply() error {
var errs []error
diff --git a/internal/resource/pkg/dnf.go b/internal/resource/pkg/dnf.go
index e970b10..34f0a5d 100644
--- a/internal/resource/pkg/dnf.go
+++ b/internal/resource/pkg/dnf.go
@@ -9,7 +9,7 @@ import (
func applyDNF(p *Package) error {
var args []string
- if p.absent {
+ 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.
diff --git a/internal/resource/pkg/dnf_test.go b/internal/resource/pkg/dnf_test.go
index 6c929f2..ddd84e6 100644
--- a/internal/resource/pkg/dnf_test.go
+++ b/internal/resource/pkg/dnf_test.go
@@ -3,6 +3,8 @@ package pkg
import (
"os"
"testing"
+
+ "codeberg.org/snonux/gonf/internal/resource/embed"
)
func TestApplyDNF(t *testing.T) {
@@ -21,7 +23,7 @@ func TestApplyDNF(t *testing.T) {
}
t.Run("Present", func(t *testing.T) {
- p.absent = false
+ p.Absent = false
p.latest = false
if err := applyDNF(p); err != nil {
t.Errorf("applyDNF Present failed: %v", err)
@@ -29,7 +31,7 @@ func TestApplyDNF(t *testing.T) {
})
t.Run("Latest", func(t *testing.T) {
- p.absent = false
+ p.Absent = false
p.latest = true
if err := applyDNF(p); err != nil {
t.Errorf("applyDNF Latest failed: %v", err)
@@ -37,7 +39,7 @@ func TestApplyDNF(t *testing.T) {
})
t.Run("Absent", func(t *testing.T) {
- p.absent = true
+ p.Absent = true
p.latest = false
if err := applyDNF(p); err != nil {
t.Errorf("applyDNF Absent failed: %v", err)
@@ -46,9 +48,9 @@ func TestApplyDNF(t *testing.T) {
t.Run("NonExistentPresent", func(t *testing.T) {
pErr := &Package{
- name: "non-existent-package-gonf-12345",
- absent: false,
- latest: false,
+ name: "non-existent-package-gonf-12345",
+ Absence: embed.Absence{Absent: false},
+ latest: false,
}
if err := applyDNF(pErr); err == nil {
t.Error("applyDNF Present should have failed for non-existent package")
@@ -57,9 +59,9 @@ func TestApplyDNF(t *testing.T) {
t.Run("NonExistentLatest", func(t *testing.T) {
pErr := &Package{
- name: "non-existent-package-gonf-12345",
- absent: false,
- latest: true,
+ name: "non-existent-package-gonf-12345",
+ Absence: embed.Absence{Absent: false},
+ latest: true,
}
if err := applyDNF(pErr); err == nil {
t.Error("applyDNF Latest should have failed for non-existent package")
@@ -68,9 +70,9 @@ func TestApplyDNF(t *testing.T) {
t.Run("NonExistentAbsent", func(t *testing.T) {
pErr := &Package{
- name: "non-existent-package-gonf-12345",
- absent: true,
- latest: false,
+ name: "non-existent-package-gonf-12345",
+ Absence: embed.Absence{Absent: true},
+ latest: false,
}
// dnf remove is typically idempotent; removing a non-existent package should not error.
if err := applyDNF(pErr); err != nil {
diff --git a/internal/resource/pkg/pkg.go b/internal/resource/pkg/pkg.go
index b010c7a..c4799c8 100644
--- a/internal/resource/pkg/pkg.go
+++ b/internal/resource/pkg/pkg.go
@@ -6,15 +6,16 @@ import (
opt "codeberg.org/snonux/gonf/api/options"
"codeberg.org/snonux/gonf/internal/resource"
+ "codeberg.org/snonux/gonf/internal/resource/embed"
)
type Package struct {
+ embed.DependsOn
+ embed.Absence
name string
- absent bool
latest bool
}
-func (p *Package) SetAbsent() { p.absent = true }
func (p *Package) SetLatest() { p.latest = true }
func (p *Package) apply() error {
@@ -41,7 +42,7 @@ func Present(name string, opts ...opt.Option) resource.Resource {
}
return resource.Register("Package", p.name,
- resource.ApplierFunc(func() error { return p.apply() }))
+ resource.ApplierFunc(func() error { return p.apply() }), p.DependsOn.IDs...)
}
func Absent(name string, opts ...opt.Option) resource.Resource {
diff --git a/internal/resource/repository.go b/internal/resource/repository.go
index 0771ef8..ece44c0 100644
--- a/internal/resource/repository.go
+++ b/internal/resource/repository.go
@@ -3,6 +3,8 @@ package resource
import (
"fmt"
"log"
+ "sort"
+ "strings"
"sync"
)
@@ -70,7 +72,10 @@ func (r *repository) apply() error {
}
visiting[id] = true
- for depID := range res.dependsOn {
+ // Visit dependencies in sorted order so the resulting apply order is
+ // stable and the log output is reproducible.
+ for _, depID := range res.sortedDependsOn() {
+ log.Printf("Resolving dependency of %v: needs %s first", res, depID)
if err := visit(depID); err != nil {
return err
}
@@ -81,14 +86,33 @@ func (r *repository) apply() error {
return nil
}
+ // Seed the traversal from a sorted list of roots so the overall order is
+ // deterministic regardless of map iteration order.
+ roots := make([]string, 0, len(r.registered))
for id := range r.registered {
+ roots = append(roots, id)
+ }
+ sort.Strings(roots)
+
+ for _, id := range roots {
if err := visit(id); err != nil {
return err
}
}
+ orderIDs := make([]string, 0, len(order))
+ for _, res := range order {
+ orderIDs = append(orderIDs, res.ID())
+ }
+ log.Printf("Resolved apply order: %s", strings.Join(orderIDs, " -> "))
+
for _, res := range order {
- log.Printf("Applying resource %v", res)
+ if deps := res.sortedDependsOn(); len(deps) > 0 {
+ log.Printf("Applying resource %v (dependencies already applied: %s)",
+ res, strings.Join(deps, ", "))
+ } else {
+ log.Printf("Applying resource %v (no dependencies)", res)
+ }
if err := res.Apply(); err != nil {
return fmt.Errorf("failed to apply %v: %w", res, err)
}
diff --git a/internal/resource/repository_test.go b/internal/resource/repository_test.go
index f939fca..ed2755e 100644
--- a/internal/resource/repository_test.go
+++ b/internal/resource/repository_test.go
@@ -48,6 +48,53 @@ func TestApply(t *testing.T) {
wantOrder: []string{"C", "B", "A"},
},
{
+ name: "independent resources apply in sorted order",
+ setup: func(r *repository, logs *[]string) {
+ // Registered/visited in a deterministic, sorted order
+ // regardless of map iteration.
+ r.registered["C"] = Resource{
+ Type: "T", Name: "C",
+ applier: &mockApplier{name: "C", logs: logs},
+ }
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ applier: &mockApplier{name: "A", logs: logs},
+ }
+ r.registered["B"] = Resource{
+ Type: "T", Name: "B",
+ applier: &mockApplier{name: "B", logs: logs},
+ }
+ },
+ wantOrder: []string{"A", "B", "C"},
+ },
+ {
+ name: "diamond dependency",
+ setup: func(r *repository, logs *[]string) {
+ // D depends on B and C; both B and C depend on A. A must run
+ // once, before B and C, which run before D.
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ applier: &mockApplier{name: "A", logs: logs},
+ }
+ r.registered["B"] = Resource{
+ Type: "T", Name: "B",
+ applier: &mockApplier{name: "B", logs: logs},
+ dependsOn: map[string]struct{}{"A": {}},
+ }
+ r.registered["C"] = Resource{
+ Type: "T", Name: "C",
+ applier: &mockApplier{name: "C", logs: logs},
+ dependsOn: map[string]struct{}{"A": {}},
+ }
+ r.registered["D"] = Resource{
+ Type: "T", Name: "D",
+ applier: &mockApplier{name: "D", logs: logs},
+ dependsOn: map[string]struct{}{"B": {}, "C": {}},
+ }
+ },
+ wantOrder: []string{"A", "B", "C", "D"},
+ },
+ {
name: "circular dependency",
setup: func(r *repository, logs *[]string) {
r.registered["A"] = Resource{
diff --git a/internal/resource/resource.go b/internal/resource/resource.go
index 2a01629..1cf451a 100644
--- a/internal/resource/resource.go
+++ b/internal/resource/resource.go
@@ -3,6 +3,7 @@ package resource
import (
"fmt"
"log"
+ "sort"
)
type Applier interface {
@@ -22,12 +23,17 @@ type Resource struct {
dependsOn map[string]struct{}
}
-func Register(type_, name string, apply Applier) Resource {
+func Register(type_, name string, apply Applier, deps ...string) Resource {
+ dependsOn := make(map[string]struct{}, len(deps))
+ for _, id := range deps {
+ dependsOn[id] = struct{}{}
+ }
+
r := Resource{
Type: type_,
Name: name,
applier: apply,
- dependsOn: make(map[string]struct{}),
+ dependsOn: dependsOn,
}
if err := getRepository().register(r); err != nil {
@@ -45,6 +51,23 @@ func (r Resource) ID() string {
return fmt.Sprintf("%s[%s]", r.Type, r.Name)
}
+// Dependencies returns this resource's own ID. It lets a single Resource be
+// used as a DependsOn target, mirroring Multi.Dependencies.
+func (r Resource) Dependencies() []string {
+ return []string{r.ID()}
+}
+
+// sortedDependsOn returns the IDs this resource depends on, sorted for stable
+// and readable log output.
+func (r Resource) sortedDependsOn() []string {
+ ids := make([]string, 0, len(r.dependsOn))
+ for id := range r.dependsOn {
+ ids = append(ids, id)
+ }
+ sort.Strings(ids)
+ return ids
+}
+
func (r Resource) Apply() error {
return r.applier.Apply()
}