| 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 | } |