-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathworker_manager.go
More file actions
326 lines (278 loc) · 10.3 KB
/
Copy pathworker_manager.go
File metadata and controls
326 lines (278 loc) · 10.3 KB
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
// SPDX-License-Identifier: Apache-2.0
package git
import (
"context"
"fmt"
"sync"
"github.com/go-logr/logr"
"sigs.k8s.io/controller-runtime/pkg/client"
configv1alpha3 "github.com/ConfigButler/gitops-reverser/api/v1alpha3"
"github.com/ConfigButler/gitops-reverser/internal/types"
"github.com/ConfigButler/gitops-reverser/internal/typeset"
)
// DefaultBranchBufferMaxBytes is the default cap on a worker's combined event
// buffer + unpushed-events memory. Operators override this via
// --branch-buffer-max-size (8Mi by default).
const DefaultBranchBufferMaxBytes int64 = 8 * 1024 * 1024
// WorkerManager manages BranchWorkers.
// Creates workers per (repo, branch), shared by multiple GitDestinations.
// Implements controller-runtime's Runnable interface for lifecycle management.
type WorkerManager struct {
Client client.Client
Log logr.Logger
branchBufferMaxBytes int64
sensitiveResources types.SensitiveResourcePolicy
mu sync.RWMutex
workers map[BranchKey]*BranchWorker
ctx context.Context
// mapper is the GVK->GVR resolver injected into every worker so store scans build a
// resource-identity inventory. It is set once at startup (SetMapper) before any
// worker is created; a nil mapper keeps workers structure-only. It is the LOCAL cluster's
// resolver and the fallback when a GitTarget names no source cluster.
mapper typeset.Lookup
// clusterMapper resolves the GVK->GVR lookup for a NAMED source cluster, so a worker
// serving a GitTarget that mirrors a remote resolves that folder against the remote's
// registry — never a union. Set once at startup (SetClusterMapper); nil in the CLI and
// in tests, which fall back to `mapper`.
clusterMapper func(clusterID string) typeset.Lookup
// sshHostKeys configures SSH host-key resolution for every worker's credential reads. Set
// once at startup (SetSSHHostKeyConfig) before any worker is created.
sshHostKeys SSHHostKeyConfig
// pathRefusal reports a refused live write plan to the GitTarget status surface. Set
// once at startup (SetPathRefusalReporter) before any worker is created; nil in the
// CLI and in tests that do not assert on the status transition.
pathRefusal PathRefusalReporter
// renderFidelityGate is shared by every worker and the watch manager. It is created with the
// manager so a target's state survives workers being recreated for the same branch.
renderFidelityGate *RenderFidelityGate
}
// NewWorkerManager creates a new worker manager.
// branchBufferMaxBytes bounds each worker's combined buffer + unpushed-events
// memory. Pass 0 (or a negative value) to use DefaultBranchBufferMaxBytes.
func NewWorkerManager(
client client.Client,
log logr.Logger,
branchBufferMaxBytes int64,
sensitiveResources types.SensitiveResourcePolicy,
) *WorkerManager {
if branchBufferMaxBytes <= 0 {
branchBufferMaxBytes = DefaultBranchBufferMaxBytes
}
return &WorkerManager{
Client: client,
Log: log,
branchBufferMaxBytes: branchBufferMaxBytes,
sensitiveResources: sensitiveResources,
workers: make(map[BranchKey]*BranchWorker),
renderFidelityGate: NewRenderFidelityGate(),
}
}
// RenderFidelityGate returns the manager-wide target gate used by branch workers. The gate is
// safe for concurrent watch and worker access.
func (m *WorkerManager) RenderFidelityGate() *RenderFidelityGate {
m.mu.RLock()
defer m.mu.RUnlock()
return m.renderFidelityGate
}
// SetMapper injects the GVK->GVR resolver used by every worker's store scan. It is
// called once at startup, before any GitTarget registers a worker, so each worker
// created by EnsureWorker carries it.
func (m *WorkerManager) SetMapper(mapper typeset.Lookup) {
m.mu.Lock()
defer m.mu.Unlock()
m.mapper = mapper
}
// SetClusterMapper injects the per-source-cluster GVK->GVR resolver used by every worker's
// store scan when a GitTarget names a source cluster. Like SetMapper, it is called once at
// startup before any worker is created, so each worker created by EnsureWorker carries it.
func (m *WorkerManager) SetClusterMapper(resolver func(clusterID string) typeset.Lookup) {
m.mu.Lock()
defer m.mu.Unlock()
m.clusterMapper = resolver
}
// SetSSHHostKeyConfig injects the SSH host-key resolution config used by every worker's credential
// reads. Like SetMapper, it is called once at startup before any worker is created.
func (m *WorkerManager) SetSSHHostKeyConfig(cfg SSHHostKeyConfig) {
m.mu.Lock()
defer m.mu.Unlock()
m.sshHostKeys = cfg
}
// SetPathRefusalReporter injects the hook every worker calls when a live write plan is
// refused, so the refusal reaches GitTarget status instead of being logged and dropped. Like
// SetMapper, it is called once at startup before any worker is created.
func (m *WorkerManager) SetPathRefusalReporter(reporter PathRefusalReporter) {
m.mu.Lock()
defer m.mu.Unlock()
m.pathRefusal = reporter
}
// RegisterTarget ensures a worker exists for the target's (provider, branch)
// and registers the target with that worker.
// This is called by GitTarget controller when a target becomes Ready.
func (m *WorkerManager) RegisterTarget(
ctx context.Context,
targetName, targetNamespace string,
providerName, providerNamespace string,
branch, path string,
) error {
if err := m.EnsureWorker(ctx, providerName, providerNamespace, branch); err != nil {
return err
}
m.Log.Info("GitTarget registered with branch worker",
"target", fmt.Sprintf("%s/%s", targetNamespace, targetName),
"workerKey", BranchKey{
RepoNamespace: providerNamespace,
RepoName: providerName,
Branch: branch,
}.String(),
"path", path)
return nil
}
// EnsureWorker ensures a worker exists for the given (provider, branch).
// Worker creation/start is protected by the manager lock.
func (m *WorkerManager) EnsureWorker(
_ context.Context,
providerName, providerNamespace string,
branch string,
) error {
m.mu.Lock()
defer m.mu.Unlock()
key := BranchKey{
RepoNamespace: providerNamespace,
RepoName: providerName,
Branch: branch,
}
if _, exists := m.workers[key]; !exists {
m.Log.Info("Creating new branch worker", "key", key.String())
worker := NewBranchWorker(
m.Client,
m.Log.WithName("branch-worker"),
providerName,
providerNamespace,
branch,
newContentWriter(m.sensitiveResources),
m.branchBufferMaxBytes,
)
// Inject the resolver before Start: the field is read only by the event-loop
// goroutine Start spawns, so setting it here (under m.mu, before that goroutine
// exists) is race-free.
worker.mapper = m.mapper
worker.clusterMapper = m.clusterMapper
worker.sshHostKeys = m.sshHostKeys
worker.pathRefusal = m.pathRefusal
worker.renderFidelityGate = m.renderFidelityGate
if err := worker.Start(m.ctx); err != nil {
return fmt.Errorf("failed to start worker for %s: %w", key.String(), err)
}
m.workers[key] = worker
}
return nil
}
// UnregisterTarget removes a GitTarget from its worker.
// Destroys the worker if it was the last target using it.
// This is called by GitTarget controller when a target is deleted.
func (m *WorkerManager) UnregisterTarget(
_, _ string,
providerName, providerNamespace string,
branch string,
) error {
m.mu.Lock()
defer m.mu.Unlock()
key := BranchKey{
RepoNamespace: providerNamespace,
RepoName: providerName,
Branch: branch,
}
worker, exists := m.workers[key]
if !exists {
return nil
}
// Worker no longer tracks targets internally - always destroy worker
// since WorkerManager handles all lifecycle decisions
m.Log.Info("Unregistering target, destroying worker", "key", key.String())
worker.Stop()
delete(m.workers, key)
return nil
}
// GetWorkerForTarget finds the worker for a target's (provider, branch).
// Returns the worker and true if found, nil and false otherwise.
// This is used by EventRouter to dispatch events to the correct worker.
func (m *WorkerManager) GetWorkerForTarget(
providerName, providerNamespace string,
branch string,
) (*BranchWorker, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
key := BranchKey{
RepoNamespace: providerNamespace,
RepoName: providerName,
Branch: branch,
}
worker, exists := m.workers[key]
return worker, exists
}
// ReconcileWorkers checks active GitTargets and cleans up orphaned workers.
// This ensures workers are removed when their GitTargets are deleted.
func (m *WorkerManager) ReconcileWorkers(ctx context.Context) error {
m.mu.Lock()
defer m.mu.Unlock()
// Get all GitTargets
var targetList configv1alpha3.GitTargetList
if err := m.Client.List(ctx, &targetList); err != nil {
return fmt.Errorf("failed to list GitTargets: %w", err)
}
// Build set of needed workers from active GitTargets
neededWorkers := make(map[BranchKey]bool)
for _, target := range targetList.Items {
// Skip deleted targets
if !target.DeletionTimestamp.IsZero() {
continue
}
// Determine namespace (Provider is always in same namespace as Target)
providerNS := target.Namespace
key := BranchKey{
RepoNamespace: providerNS,
RepoName: target.Spec.ProviderRef.Name,
Branch: target.Spec.Branch,
}
neededWorkers[key] = true
}
// Cleanup orphaned workers
for key, worker := range m.workers {
if !neededWorkers[key] {
m.Log.Info("Cleaning up orphaned worker", "key", key.String())
worker.Stop()
delete(m.workers, key)
}
}
m.Log.V(1).Info("Worker reconciliation complete",
"activeWorkers", len(m.workers),
"neededWorkers", len(neededWorkers))
return nil
}
// Start implements manager.Runnable interface.
// This is called by controller-runtime when the manager starts.
func (m *WorkerManager) Start(ctx context.Context) error {
// Publish the context under m.mu: EnsureWorker reads m.ctx under the same lock,
// so guarding the write makes that read race-free regardless of call ordering.
m.mu.Lock()
m.ctx = ctx
m.mu.Unlock()
m.Log.Info("WorkerManager started")
<-ctx.Done()
m.Log.Info("WorkerManager shutting down")
m.mu.Lock()
defer m.mu.Unlock()
// Stop all workers gracefully
for key, worker := range m.workers {
m.Log.Info("Stopping worker for shutdown", "key", key.String())
worker.Stop()
}
m.workers = make(map[BranchKey]*BranchWorker)
m.Log.Info("WorkerManager stopped")
return nil
}
// NeedLeaderElection ensures only the elected leader manages workers.
// This prevents multiple pods from managing the same workers.
func (m *WorkerManager) NeedLeaderElection() bool {
return true
}