master
go 374 lines 10.2 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretsctl
4
5 import (
6 "encoding/json"
7 "errors"
8 "fmt"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
13 "gopkg.in/yaml.v2"
14 )
15
16 func dyncfgSecretStoreTemplateCmds() string {
17 return dyncfg.JoinCommands(dyncfg.CommandAdd, dyncfg.CommandSchema, dyncfg.CommandUserconfig)
18 }
19
20 func (c *Controller) SeqExec(fn dyncfg.Function) {
21 switch fn.Command() {
22 case dyncfg.CommandSchema:
23 c.dyncfgCmdSchema(fn)
24 case dyncfg.CommandGet:
25 c.dyncfgCmdGet(fn)
26 case dyncfg.CommandAdd:
27 c.dyncfgCmdAdd(fn)
28 case dyncfg.CommandUpdate:
29 // TODO: file/user -> dyncfg conversion currently reuses generic update ordering,
30 // which can tear down the old live store before the override is active.
31 c.handler.CmdUpdate(fn)
32 case dyncfg.CommandTest:
33 c.dyncfgCmdTest(fn)
34 case dyncfg.CommandUserconfig:
35 c.dyncfgCmdUserconfig(fn)
36 case dyncfg.CommandRemove:
37 c.dyncfgCmdRemove(fn)
38 default:
39 c.Warningf("dyncfg: function '%s' command '%s' not implemented", fn.Fn().Name, fn.Command())
40 c.api.SendCodef(fn, 501, "Function '%s' command '%s' is not implemented.", fn.Fn().Name, fn.Command())
41 }
42 }
43
44 func (c *Controller) dyncfgCmdAdd(fn dyncfg.Function) {
45 if err := fn.ValidateArgs(3); err != nil {
46 c.api.SendCodef(fn, 400, "%v", err)
47 return
48 }
49
50 key, name, ok := c.cb.ExtractKey(fn)
51 if !ok {
52 c.api.SendCodef(fn, 400, "invalid job ID format.")
53 return
54 }
55 if _, exists := c.lookup(key); exists {
56 c.api.SendCodef(fn, 409, "The specified secretstore '%s' already exists.", key)
57 return
58 }
59 if err := fn.ValidateHasPayload(); err != nil {
60 c.api.SendCodef(fn, 400, "%v", err)
61 return
62 }
63 if err := dyncfg.JobNameRuleAllowDots(name); err != nil {
64 c.api.SendCodef(fn, 400, "invalid job name '%s': %v.", name, err)
65 return
66 }
67
68 kind, ok := c.dyncfgExtractSecretStoreKindFromTemplateID(fn.ID())
69 if !ok {
70 c.api.SendCodef(fn, 400, "Invalid template ID for secretstore add: %s.", fn.ID())
71 return
72 }
73
74 rawCfg, err := c.dyncfgSecretStoreConfigFromPayload(fn, name, kind)
75 if err != nil {
76 c.api.SendCodef(fn, 400, "%v", err)
77 return
78 }
79 if err := rawCfg.Validate(); err != nil {
80 c.api.SendCodef(fn, 400, "%v", err)
81 return
82 }
83
84 cfg, prepErr := c.prepareConfigCandidate(rawCfg)
85 c.seen.Add(cfg)
86 entry := &dyncfg.Entry[secretstore.Config]{Cfg: cfg, Status: dyncfg.StatusFailed}
87 c.exposed.Add(entry)
88
89 code := 200
90 msg := ""
91 if prepErr == nil {
92 if err := c.cb.Start(cfg); err == nil {
93 entry.Status = dyncfg.StatusRunning
94 msg = c.cb.TakeCommandMessage()
95 } else {
96 prepErr = err
97 }
98 }
99 if prepErr != nil {
100 code = secretStoreCommandCode(prepErr)
101 msg = prepErr.Error()
102 }
103
104 c.api.SendCodef(fn, code, "%s", msg)
105 c.handler.NotifyJobCreate(cfg, entry.Status)
106 }
107
108 func (c *Controller) dyncfgCmdSchema(fn dyncfg.Function) {
109 kind, err := c.dyncfgResolveSecretStoreKind(fn.ID())
110 if err != nil {
111 c.api.SendCodef(fn, secretStoreErrorCode(err), "%v", err)
112 return
113 }
114
115 schema, ok := c.service.Schema(kind)
116 if !ok {
117 c.api.SendCodef(fn, 404, "The specified secretstore kind '%s' is not supported.", kind)
118 return
119 }
120
121 c.api.SendJSON(fn, schema)
122 }
123
124 func (c *Controller) dyncfgCmdGet(fn dyncfg.Function) {
125 storeKey, ok := c.dyncfgExtractSecretStoreKey(fn.ID())
126 if !ok {
127 c.api.SendCodef(fn, 400, "Invalid ID format for secretstore get: %s.", fn.ID())
128 return
129 }
130
131 entry, ok := c.lookup(storeKey)
132 if !ok {
133 c.api.SendCodef(fn, 404, "The specified secretstore '%s' is not configured.", storeKey)
134 return
135 }
136
137 cfg, err := c.dyncfgTypedConfigFromRaw(entry.Cfg)
138 if err != nil {
139 c.api.SendCodef(fn, 500, "Failed to materialize secretstore configuration: %v.", err)
140 return
141 }
142
143 bs, err := json.Marshal(cfg)
144 if err != nil {
145 c.api.SendCodef(fn, 500, "Failed to convert configuration into JSON: %v.", err)
146 return
147 }
148
149 c.api.SendJSON(fn, string(bs))
150 }
151
152 func (c *Controller) dyncfgCmdTest(fn dyncfg.Function) {
153 storeKey, ok := c.dyncfgExtractSecretStoreKey(fn.ID())
154 if !ok {
155 c.api.SendCodef(fn, 400, "Invalid ID format for secretstore test: %s.", fn.ID())
156 return
157 }
158
159 if !fn.HasPayload() {
160 if err := c.validateStored(storeKey); err != nil {
161 c.api.SendCodef(fn, secretStoreErrorCode(err), "%v", err)
162 return
163 }
164 c.dyncfgSendSecretStoreTestImpactMessage(fn, c.affectedJobsFor(storeKey), c.restartableAffectedJobsFor(storeKey), true)
165 return
166 }
167
168 entry, ok := c.lookup(storeKey)
169 if !ok {
170 c.api.SendCodef(fn, 404, "The specified secretstore '%s' is not configured.", storeKey)
171 return
172 }
173
174 cfg, err := c.dyncfgSecretStoreConfigFromPayload(fn, entry.Cfg.Name(), entry.Cfg.Kind())
175 if err != nil {
176 c.api.SendCodef(fn, 400, "%v", err)
177 return
178 }
179
180 cfg, err = c.prepareConfigCandidate(cfg)
181 if err != nil {
182 c.api.SendCodef(fn, 400, "%v", err)
183 return
184 }
185
186 if cfg.Hash() == entry.Cfg.Hash() {
187 c.api.SendCodef(fn, 202, "Submitted configuration does not change the active secretstore.")
188 return
189 }
190
191 c.dyncfgSendSecretStoreTestImpactMessage(fn, c.affectedJobsFor(storeKey), c.restartableAffectedJobsFor(storeKey), false)
192 }
193
194 func (c *Controller) dyncfgCmdUserconfig(fn dyncfg.Function) {
195 kind, err := c.dyncfgResolveSecretStoreKind(fn.ID())
196 if err != nil {
197 c.api.SendCodef(fn, secretStoreErrorCode(err), "%v", err)
198 return
199 }
200 if err := fn.ValidateHasPayload(); err != nil {
201 c.api.SendCodef(fn, 400, "%v", err)
202 return
203 }
204
205 cfg, err := c.dyncfgTypedConfigFromPayload(fn, kind)
206 if err != nil {
207 c.api.SendCodef(fn, 400, "Invalid configuration format. Failed to create configuration from payload: %v.", err)
208 return
209 }
210
211 bs, err := yaml.Marshal(cfg)
212 if err != nil {
213 c.api.SendCodef(fn, 500, "Failed to convert configuration into YAML: %v.", err)
214 return
215 }
216
217 c.api.SendYAML(fn, string(bs))
218 }
219
220 func (c *Controller) dyncfgCmdRemove(fn dyncfg.Function) {
221 storeKey, ok := c.dyncfgExtractSecretStoreKey(fn.ID())
222 if !ok {
223 c.api.SendCodef(fn, 400, "Invalid ID format for secretstore remove: %s.", fn.ID())
224 return
225 }
226
227 if _, ok := c.lookup(storeKey); !ok {
228 c.api.SendCodef(fn, 404, "The specified secretstore '%s' is not configured.", storeKey)
229 return
230 }
231
232 if affected := formatAffectedJobs(c.affectedJobsFor(storeKey)); affected != "" {
233 c.api.SendCodef(fn, 409, "The specified secretstore '%s' is used by jobs (%s).", storeKey, affected)
234 return
235 }
236
237 c.handler.CmdRemove(fn)
238 }
239
240 func (c *Controller) dyncfgTypedConfigFromPayload(fn dyncfg.Function, kind secretstore.StoreKind) (any, error) {
241 cfg, err := c.dyncfgNewTypedConfig(kind)
242 if err != nil {
243 return nil, err
244 }
245 if err := fn.UnmarshalPayload(cfg); err != nil {
246 return nil, err
247 }
248 return cfg, nil
249 }
250
251 func (c *Controller) dyncfgTypedConfigFromRaw(rawCfg secretstore.Config) (any, error) {
252 cfg, err := c.dyncfgNewTypedConfig(rawCfg.Kind())
253 if err != nil {
254 return nil, err
255 }
256
257 bs, err := yaml.Marshal(rawCfg)
258 if err != nil {
259 return nil, err
260 }
261 if err := yaml.Unmarshal(bs, cfg); err != nil {
262 return nil, err
263 }
264 return cfg, nil
265 }
266
267 func (c *Controller) dyncfgNewTypedConfig(kind secretstore.StoreKind) (any, error) {
268 if c.service == nil {
269 return nil, fmt.Errorf("secretstore service is not available")
270 }
271
272 store, ok := c.service.New(kind)
273 if !ok {
274 return nil, fmt.Errorf("the specified secretstore kind '%s' is not supported", kind)
275 }
276
277 cfg := store.Configuration()
278 if cfg == nil {
279 return nil, fmt.Errorf("secretstore kind '%s' does not provide configuration", kind)
280 }
281 return cfg, nil
282 }
283
284 func (c *Controller) dyncfgResolveSecretStoreKind(id string) (secretstore.StoreKind, error) {
285 if kind, ok := c.dyncfgExtractSecretStoreKindFromTemplateID(id); ok {
286 return kind, nil
287 }
288 storeKey, ok := c.dyncfgExtractSecretStoreKey(id)
289 if !ok {
290 return "", fmt.Errorf("invalid secretstore ID format: %s", id)
291 }
292 entry, ok := c.lookup(storeKey)
293 if !ok {
294 return "", fmt.Errorf("%w: %s", secretstore.ErrStoreNotFound, storeKey)
295 }
296 return entry.Cfg.Kind(), nil
297 }
298
299 func (c *Controller) dyncfgExtractSecretStoreKindFromTemplateID(id string) (secretstore.StoreKind, bool) {
300 return c.cb.deps.extractSecretStoreKindFromTemplateID(id)
301 }
302
303 func (c *Controller) dyncfgExtractSecretStoreKey(id string) (string, bool) {
304 return c.cb.deps.extractSecretStoreKey(id)
305 }
306
307 func (c *Controller) dyncfgSecretStoreConfigFromPayload(fn dyncfg.Function, name string, kind secretstore.StoreKind) (secretstore.Config, error) {
308 return c.cb.deps.secretStoreConfigFromPayload(fn, name, kind)
309 }
310
311 func formatAffectedJobs(refs []secretstore.JobRef) string {
312 if len(refs) == 0 {
313 return ""
314 }
315
316 var b strings.Builder
317 for i, ref := range refs {
318 if i > 0 {
319 b.WriteString(", ")
320 }
321 if ref.Display != "" {
322 b.WriteString(ref.Display)
323 } else {
324 b.WriteString(ref.ID)
325 }
326 }
327 return b.String()
328 }
329
330 func (c *Controller) dyncfgSendSecretStoreTestImpactMessage(fn dyncfg.Function, refs, restartable []secretstore.JobRef, validationOnly bool) {
331 affected := formatAffectedJobs(refs)
332 restartableAffected := formatAffectedJobs(restartable)
333 if validationOnly {
334 if affected != "" {
335 if restartableAffected != "" {
336 c.api.SendCodef(fn, 202, "Stored configuration is valid. This secretstore is used by jobs: %s. Running or failed jobs that would be restarted automatically by a change: %s.", affected, restartableAffected)
337 return
338 }
339 c.api.SendCodef(fn, 202, "Stored configuration is valid. This secretstore is used by jobs: %s. No running or failed jobs would be restarted automatically by a change.", affected)
340 return
341 }
342 c.api.SendCodef(fn, 202, "Stored configuration is valid. No jobs are currently using this secretstore.")
343 return
344 }
345
346 if affected != "" {
347 if restartableAffected != "" {
348 c.api.SendCodef(fn, 202, "Updated configuration is used by jobs: %s. Running or failed jobs that would be restarted automatically: %s.", affected, restartableAffected)
349 return
350 }
351 c.api.SendCodef(fn, 202, "Updated configuration is used by jobs: %s. No running or failed jobs would be restarted automatically.", affected)
352 return
353 }
354 c.api.SendCodef(fn, 202, "No jobs currently use this secretstore.")
355 }
356
357 func secretStoreErrorCode(err error) int {
358 switch {
359 case errors.Is(err, secretstore.ErrStoreExists):
360 return 409
361 case errors.Is(err, secretstore.ErrStoreNotFound):
362 return 404
363 default:
364 return 400
365 }
366 }
367
368 func secretStoreCommandCode(err error) int {
369 var ce interface{ Code() int }
370 if errors.As(err, &ce) {
371 return ce.Code()
372 }
373 return secretStoreErrorCode(err)
374 }