master
go 337 lines 7.64 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package jobmgr
4
5 import (
6 "fmt"
7 "regexp"
8 "sort"
9 "strings"
10 "sync"
11
12 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/confgroup"
14 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
15 )
16
17 var (
18 reSecretRef = regexp.MustCompile(`\$\{([^}]+)\}`)
19 reUpperShorthand = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`)
20 )
21
22 type secretStoreDeps struct {
23 mu sync.RWMutex
24
25 jobs map[string]*secretStoreJobState
26
27 // storeKey -> internal job key -> ref
28 exposed map[string]map[string]secretstore.JobRef
29 running map[string]map[string]secretstore.JobRef
30 }
31
32 type secretStoreJobState struct {
33 display string
34 stores map[string]struct{}
35 running bool
36 }
37
38 func newSecretStoreDeps() *secretStoreDeps {
39 return &secretStoreDeps{
40 jobs: make(map[string]*secretStoreJobState),
41 exposed: make(map[string]map[string]secretstore.JobRef),
42 running: make(map[string]map[string]secretstore.JobRef),
43 }
44 }
45
46 func (d *secretStoreDeps) SetActiveJobStores(internalKey, display string, storeKeys []string) {
47 internalKey = strings.TrimSpace(internalKey)
48 if internalKey == "" {
49 return
50 }
51 if display == "" {
52 display = internalKey
53 }
54
55 normalized := normalizeStoreKeys(storeKeys)
56
57 d.mu.Lock()
58 defer d.mu.Unlock()
59
60 state, ok := d.jobs[internalKey]
61 if !ok {
62 state = &secretStoreJobState{}
63 d.jobs[internalKey] = state
64 }
65
66 for storeKey := range state.stores {
67 d.removeRefLocked(d.exposed, storeKey, internalKey)
68 if state.running {
69 d.removeRefLocked(d.running, storeKey, internalKey)
70 }
71 }
72
73 state.display = display
74 state.stores = make(map[string]struct{}, len(normalized))
75 for _, storeKey := range normalized {
76 state.stores[storeKey] = struct{}{}
77 ref := secretstore.JobRef{ID: internalKey, Display: display}
78 d.addRefLocked(d.exposed, storeKey, ref)
79 if state.running {
80 d.addRefLocked(d.running, storeKey, ref)
81 }
82 }
83 }
84
85 func (d *secretStoreDeps) RemoveActiveJob(internalKey string) {
86 internalKey = strings.TrimSpace(internalKey)
87 if internalKey == "" {
88 return
89 }
90
91 d.mu.Lock()
92 defer d.mu.Unlock()
93
94 state, ok := d.jobs[internalKey]
95 if !ok {
96 return
97 }
98
99 for storeKey := range state.stores {
100 d.removeRefLocked(d.exposed, storeKey, internalKey)
101 d.removeRefLocked(d.running, storeKey, internalKey)
102 }
103 delete(d.jobs, internalKey)
104 }
105
106 func (d *secretStoreDeps) setRunning(internalKey string, running bool) {
107 internalKey = strings.TrimSpace(internalKey)
108 if internalKey == "" {
109 return
110 }
111
112 d.mu.Lock()
113 defer d.mu.Unlock()
114
115 state, ok := d.jobs[internalKey]
116 if !ok {
117 if !running {
118 return
119 }
120 state = &secretStoreJobState{display: internalKey, stores: map[string]struct{}{}}
121 d.jobs[internalKey] = state
122 }
123
124 if state.running == running {
125 return
126 }
127
128 state.running = running
129 for storeKey := range state.stores {
130 ref := secretstore.JobRef{ID: internalKey, Display: state.display}
131 if running {
132 d.addRefLocked(d.running, storeKey, ref)
133 } else {
134 d.removeRefLocked(d.running, storeKey, internalKey)
135 }
136 }
137 }
138
139 func (d *secretStoreDeps) Impacted(storeKey string) (exposed []secretstore.JobRef, running []secretstore.JobRef) {
140 storeKey = strings.TrimSpace(storeKey)
141 if storeKey == "" {
142 return nil, nil
143 }
144
145 d.mu.RLock()
146 defer d.mu.RUnlock()
147
148 exposed = collectSortedRefs(d.exposed[storeKey])
149 running = collectSortedRefs(d.running[storeKey])
150 return exposed, running
151 }
152
153 func (d *secretStoreDeps) addRefLocked(index map[string]map[string]secretstore.JobRef, storeKey string, ref secretstore.JobRef) {
154 jobs, ok := index[storeKey]
155 if !ok {
156 jobs = make(map[string]secretstore.JobRef)
157 index[storeKey] = jobs
158 }
159 jobs[ref.ID] = ref
160 }
161
162 func (d *secretStoreDeps) removeRefLocked(index map[string]map[string]secretstore.JobRef, storeKey, internalKey string) {
163 jobs, ok := index[storeKey]
164 if !ok {
165 return
166 }
167 delete(jobs, internalKey)
168 if len(jobs) == 0 {
169 delete(index, storeKey)
170 }
171 }
172
173 func collectSortedRefs(m map[string]secretstore.JobRef) []secretstore.JobRef {
174 if len(m) == 0 {
175 return nil
176 }
177 refs := make([]secretstore.JobRef, 0, len(m))
178 for _, ref := range m {
179 refs = append(refs, ref)
180 }
181 sort.Slice(refs, func(i, j int) bool {
182 if refs[i].ID == refs[j].ID {
183 return refs[i].Display < refs[j].Display
184 }
185 return refs[i].ID < refs[j].ID
186 })
187 return refs
188 }
189
190 func normalizeStoreKeys(storeKeys []string) []string {
191 if len(storeKeys) == 0 {
192 return nil
193 }
194 seen := make(map[string]struct{}, len(storeKeys))
195 for _, key := range storeKeys {
196 kind, name, err := secretstore.ParseStoreKey(key)
197 if err != nil {
198 continue
199 }
200 seen[secretstore.StoreKey(kind, name)] = struct{}{}
201 }
202 if len(seen) == 0 {
203 return nil
204 }
205 out := make([]string, 0, len(seen))
206 for key := range seen {
207 out = append(out, key)
208 }
209 sort.Strings(out)
210 return out
211 }
212
213 func extractSecretStoreKeys(cfg confgroup.Config) []string {
214 seen := make(map[string]struct{})
215 extractSecretStoreKeysFromValue(cfg, seen)
216 if len(seen) == 0 {
217 return nil
218 }
219 keys := make([]string, 0, len(seen))
220 for key := range seen {
221 keys = append(keys, key)
222 }
223 sort.Strings(keys)
224 return keys
225 }
226
227 func extractSecretStoreKeysFromValue(v any, seen map[string]struct{}) {
228 switch value := v.(type) {
229 case string:
230 extractSecretStoreKeysFromString(value, seen)
231 case confgroup.Config:
232 for k, entry := range value {
233 if isSecretStoreInternalKey(k) {
234 continue
235 }
236 extractSecretStoreKeysFromValue(entry, seen)
237 }
238 case map[string]any:
239 for k, entry := range value {
240 if isSecretStoreInternalKey(k) {
241 continue
242 }
243 extractSecretStoreKeysFromValue(entry, seen)
244 }
245 case map[any]any:
246 for rawKey, entry := range value {
247 if key, ok := rawKey.(string); ok && isSecretStoreInternalKey(key) {
248 continue
249 }
250 extractSecretStoreKeysFromValue(entry, seen)
251 }
252 case []any:
253 for _, entry := range value {
254 extractSecretStoreKeysFromValue(entry, seen)
255 }
256 }
257 }
258
259 func extractSecretStoreKeysFromString(value string, seen map[string]struct{}) {
260 if !strings.Contains(value, "${") {
261 return
262 }
263 matches := reSecretRef.FindAllStringSubmatch(value, -1)
264 for _, match := range matches {
265 if len(match) < 2 {
266 continue
267 }
268 inner := match[1]
269 scheme, rest, hasScheme := strings.Cut(inner, ":")
270 if !hasScheme {
271 if reUpperShorthand.MatchString(inner) {
272 continue
273 }
274 continue
275 }
276 if scheme != "store" {
277 continue
278 }
279 kindPart, tail, ok := strings.Cut(rest, ":")
280 if !ok {
281 continue
282 }
283 namePart, _, ok := strings.Cut(tail, ":")
284 if !ok {
285 continue
286 }
287 kind := secretstore.StoreKind(strings.TrimSpace(kindPart))
288 name := strings.TrimSpace(namePart)
289 if !kind.IsValid() {
290 continue
291 }
292 if err := dyncfg.JobNameRuleAllowDots(name); err != nil {
293 continue
294 }
295 seen[secretstore.StoreKey(kind, name)] = struct{}{}
296 }
297 }
298
299 func isSecretStoreInternalKey(k string) bool {
300 return strings.HasPrefix(k, "__") && strings.HasSuffix(k, "__")
301 }
302
303 func secretStoreDisplay(cfg confgroup.Config) string {
304 module := strings.TrimSpace(cfg.Module())
305 job := strings.TrimSpace(cfg.Name())
306 switch {
307 case module != "" && job != "":
308 return fmt.Sprintf("%s:%s", module, job)
309 case cfg.FullName() != "":
310 return cfg.FullName()
311 default:
312 return job
313 }
314 }
315
316 func (m *Manager) syncSecretStoreDepsForConfig(cfg confgroup.Config) {
317 if m.secretStoreDeps == nil {
318 return
319 }
320 m.secretStoreDeps.SetActiveJobStores(cfg.FullName(), secretStoreDisplay(cfg), extractSecretStoreKeys(cfg))
321 }
322
323 func (m *Manager) syncSecretStoreDepsByFunction(fn dyncfg.Function) {
324 if m.secretStoreDeps == nil || m.collectorCallbacks == nil {
325 return
326 }
327 key, _, ok := m.collectorCallbacks.ExtractKey(fn)
328 if !ok {
329 return
330 }
331 entry, ok := m.collectorExposed.LookupByKey(key)
332 if !ok {
333 m.secretStoreDeps.RemoveActiveJob(key)
334 return
335 }
336 m.syncSecretStoreDepsForConfig(entry.Cfg)
337 }