-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathworker_manager.go
More file actions
281 lines (238 loc) · 8.11 KB
/
Copy pathworker_manager.go
File metadata and controls
281 lines (238 loc) · 8.11 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
// 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.
mapper 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
}
// 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),
}
}
// 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
}
// 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
}
// 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.sshHostKeys = m.sshHostKeys
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
}