summaryrefslogtreecommitdiff
path: root/internal/resource/repository.go
blob: ece44c0c1df4ddf0c80ad96865262aaded0fcf01 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package resource

import (
	"fmt"
	"log"
	"sort"
	"strings"
	"sync"
)

var (
	repo repository
	once sync.Once
)

func getRepository() *repository {
	once.Do(func() {
		repo = newRepository()
	})
	return &repo
}

func ResetRepository() {
	repo = newRepository()
}

type repository struct {
	registered map[string]Resource
	mu         sync.Mutex
}

func newRepository() repository {
	return repository{
		registered: make(map[string]Resource),
	}
}

func (r *repository) register(res Resource) error {
	r.mu.Lock()
	defer r.mu.Unlock()

	if _, exists := r.registered[res.ID()]; exists {
		return fmt.Errorf("resource %v already registered", res)
	}

	r.registered[res.ID()] = res
	log.Printf("Registered resource %v\n", res)

	return nil
}

func (r *repository) apply() error {
	r.mu.Lock()
	defer r.mu.Unlock()

	visited := make(map[string]bool)
	visiting := make(map[string]bool)
	var order []Resource

	var visit func(id string) error
	visit = func(id string) error {
		if visiting[id] {
			return fmt.Errorf("circular dependency detected involving %s", id)
		}
		if visited[id] {
			return nil
		}

		res, ok := r.registered[id]
		if !ok {
			return fmt.Errorf("resource %s is depended upon but not registered", id)
		}

		visiting[id] = true
		// 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
			}
		}
		delete(visiting, id)
		visited[id] = true
		order = append(order, res)
		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 {
		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)
		}
	}

	return nil
}

func Apply() error {
	return getRepository().apply()
}