master
go 234 lines 6.77 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretsctl
4
5 import (
6 "context"
7 "errors"
8 "fmt"
9 "strings"
10
11 "github.com/netdata/netdata/go/plugins/logger"
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 type secretStoreCallbacks struct {
18 deps secretStoreCallbackDeps
19 // commandMessage is written by Start/Update/Stop and consumed by
20 // TakeCommandMessage. Safety relies on callbacks remaining serialized by
21 // the jobmgr dyncfg command flow.
22 commandMessage string
23 }
24
25 type secretStoreCallbackDeps struct {
26 pluginName string
27 log *logger.Logger
28 service secretstore.Service
29 restartDependentJobs func(string) string
30 }
31
32 type codedError struct {
33 err error
34 code int
35 }
36
37 func (e *codedError) Error() string { return e.err.Error() }
38 func (e *codedError) Unwrap() error { return e.err }
39 func (e *codedError) Code() int { return e.code }
40
41 func newSecretStoreCallbacks(deps secretStoreCallbackDeps) *secretStoreCallbacks {
42 return &secretStoreCallbacks{deps: deps}
43 }
44
45 func (d secretStoreCallbackDeps) restartDependentJobsMessage(storeKey string) string {
46 if d.restartDependentJobs == nil {
47 return ""
48 }
49 return d.restartDependentJobs(storeKey)
50 }
51
52 func (d secretStoreCallbackDeps) extractSecretStoreKindFromTemplateID(id string) (secretstore.StoreKind, bool) {
53 rest, ok := strings.CutPrefix(id, fmt.Sprintf(dyncfgSecretStorePrefixf, d.pluginName))
54 if !ok || rest == "" || strings.Contains(rest, ":") {
55 return "", false
56 }
57 kind := secretstore.StoreKind(rest)
58 if d.service == nil {
59 return "", false
60 }
61 _, ok = d.service.DisplayName(kind)
62 return kind, ok
63 }
64
65 func (d secretStoreCallbackDeps) extractSecretStoreKey(id string) (string, bool) {
66 rest, ok := strings.CutPrefix(id, fmt.Sprintf(dyncfgSecretStorePrefixf, d.pluginName))
67 if !ok || rest == "" {
68 return "", false
69 }
70 kind, name, err := secretstore.ParseStoreKey(rest)
71 if err != nil {
72 return "", false
73 }
74 return secretstore.StoreKey(kind, name), true
75 }
76
77 func (d secretStoreCallbackDeps) secretStoreConfigFromPayload(fn dyncfg.Function, name string, kind secretstore.StoreKind) (secretstore.Config, error) {
78 if err := fn.ValidateHasPayload(); err != nil {
79 return nil, err
80 }
81
82 var payload secretstore.Config
83 if err := fn.UnmarshalPayload(&payload); err != nil {
84 return nil, fmt.Errorf("invalid configuration format: %w", err)
85 }
86 if payload == nil {
87 payload = secretstore.Config{}
88 }
89 payload.SetName(name)
90 payload.SetKind(kind)
91 payload.SetSource(confgroup.TypeDyncfg)
92 payload.SetSourceType(confgroup.TypeDyncfg)
93 return payload, nil
94 }
95
96 func (d secretStoreCallbackDeps) validateSecretStoreConfig(cfg secretstore.Config) error {
97 if err := cfg.Validate(); err != nil {
98 return err
99 }
100 if d.service == nil {
101 return fmt.Errorf("secretstore service is not available")
102 }
103 return d.service.Validate(d.resolveContext(cfg), cfg)
104 }
105
106 func (d secretStoreCallbackDeps) resolveContext(cfg secretstore.Config) context.Context {
107 return secretStoreResolveContext(d.log, cfg)
108 }
109
110 func (d secretStoreCallbackDeps) dyncfgSecretStoreID(id string) string {
111 return fmt.Sprintf("%s%s", fmt.Sprintf(dyncfgSecretStorePrefixf, d.pluginName), id)
112 }
113
114 func (cb *secretStoreCallbacks) ExtractKey(fn dyncfg.Function) (key, name string, ok bool) {
115 if fn.Command() == dyncfg.CommandAdd {
116 kind, kindOK := cb.deps.extractSecretStoreKindFromTemplateID(fn.ID())
117 name = fn.JobName()
118 if !kindOK || name == "" {
119 return "", "", false
120 }
121 return secretstore.StoreKey(kind, name), name, true
122 }
123
124 key, ok = cb.deps.extractSecretStoreKey(fn.ID())
125 if !ok {
126 return "", "", false
127 }
128 _, name, err := secretstore.ParseStoreKey(key)
129 if err != nil {
130 return "", "", false
131 }
132 return key, name, true
133 }
134
135 func (cb *secretStoreCallbacks) ValidateJobName(name string) error {
136 return dyncfg.JobNameRuleAllowDots(name)
137 }
138
139 func (cb *secretStoreCallbacks) ParseAndValidate(fn dyncfg.Function, name string) (secretstore.Config, error) {
140 var kind secretstore.StoreKind
141 if fn.Command() == dyncfg.CommandAdd {
142 var ok bool
143 kind, ok = cb.deps.extractSecretStoreKindFromTemplateID(fn.ID())
144 if !ok {
145 return nil, fmt.Errorf("invalid template ID for secretstore add: %s", fn.ID())
146 }
147 } else {
148 key, ok := cb.deps.extractSecretStoreKey(fn.ID())
149 if !ok {
150 return nil, fmt.Errorf("invalid secretstore ID: %s", fn.ID())
151 }
152 var err error
153 kind, name, err = secretstore.ParseStoreKey(key)
154 if err != nil {
155 return nil, err
156 }
157 }
158
159 cfg, err := cb.deps.secretStoreConfigFromPayload(fn, name, kind)
160 if err != nil {
161 return nil, err
162 }
163 if err := cb.deps.validateSecretStoreConfig(cfg); err != nil {
164 return nil, err
165 }
166 return cfg, nil
167 }
168
169 func (cb *secretStoreCallbacks) Start(cfg secretstore.Config) error {
170 cb.commandMessage = ""
171 key := cfg.ExposedKey()
172 if cb.deps.service == nil {
173 return &codedError{err: fmt.Errorf("secretstore service is not available"), code: 400}
174 }
175
176 if _, ok := cb.deps.service.GetStatus(key); ok {
177 if err := cb.deps.service.Update(cb.deps.resolveContext(cfg), key, cfg); err != nil {
178 return &codedError{err: err, code: secretStoreErrorCode(err)}
179 }
180 } else if err := cb.deps.service.Add(cb.deps.resolveContext(cfg), cfg); err != nil {
181 return &codedError{err: err, code: secretStoreErrorCode(err)}
182 }
183
184 cb.commandMessage = cb.deps.restartDependentJobsMessage(key)
185 return nil
186 }
187
188 func (cb *secretStoreCallbacks) Update(oldCfg, newCfg secretstore.Config) error {
189 cb.commandMessage = ""
190 key := oldCfg.ExposedKey()
191 if cb.deps.service == nil {
192 return &codedError{err: fmt.Errorf("secretstore service is not available"), code: 400}
193 }
194
195 if _, ok := cb.deps.service.GetStatus(key); ok {
196 if err := cb.deps.service.Update(cb.deps.resolveContext(newCfg), key, newCfg); err != nil {
197 return &codedError{err: err, code: secretStoreErrorCode(err)}
198 }
199 } else if err := cb.deps.service.Add(cb.deps.resolveContext(newCfg), newCfg); err != nil {
200 return &codedError{err: err, code: secretStoreErrorCode(err)}
201 }
202
203 cb.commandMessage = cb.deps.restartDependentJobsMessage(key)
204 return nil
205 }
206
207 func (cb *secretStoreCallbacks) Stop(cfg secretstore.Config) {
208 cb.commandMessage = ""
209 key := cfg.ExposedKey()
210 if cb.deps.service == nil {
211 return
212 }
213
214 if err := cb.deps.service.Remove(key); err != nil {
215 if errors.Is(err, secretstore.ErrStoreNotFound) {
216 return
217 }
218 return
219 }
220 cb.commandMessage = cb.deps.restartDependentJobsMessage(key)
221 }
222
223 func (*secretStoreCallbacks) OnStatusChange(*dyncfg.Entry[secretstore.Config], dyncfg.Status, dyncfg.Function) {
224 }
225
226 func (cb *secretStoreCallbacks) TakeCommandMessage() string {
227 msg := strings.TrimSpace(cb.commandMessage)
228 cb.commandMessage = ""
229 return msg
230 }
231
232 func (cb *secretStoreCallbacks) ConfigID(cfg secretstore.Config) string {
233 return cb.deps.dyncfgSecretStoreID(cfg.ExposedKey())
234 }