master
go 350 lines 9.63 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretstore_test
4
5 import (
6 "context"
7 "encoding/json"
8 "errors"
9 "sync"
10 "sync/atomic"
11 "testing"
12
13 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 )
17
18 type fakePublished struct {
19 blockOnCtx *atomic.Bool
20 requireNonNilCtx *atomic.Bool
21 }
22
23 func (p *fakePublished) Resolve(ctx context.Context, req secretstore.ResolveRequest) (string, error) {
24 if p.requireNonNilCtx != nil {
25 p.requireNonNilCtx.Store(ctx != nil)
26 if ctx == nil {
27 return "", errors.New("nil context")
28 }
29 }
30 if p.blockOnCtx != nil && p.blockOnCtx.Load() {
31 <-ctx.Done()
32 return "", ctx.Err()
33 }
34 return req.Operand, nil
35 }
36
37 type fakeConfig struct {
38 Auth map[string]any `json:"auth,omitempty" yaml:"auth,omitempty"`
39 }
40
41 type fakeStore struct {
42 cfg fakeConfig
43 failInit *atomic.Bool
44 blockOnCtx *atomic.Bool
45 requireNonNilCtx *atomic.Bool
46 published secretstore.PublishedStore
47 }
48
49 func (s *fakeStore) Configuration() any { return &s.cfg }
50 func (s *fakeStore) Publish() secretstore.PublishedStore {
51 return s.published
52 }
53
54 func (s *fakeStore) Init(context.Context) error {
55 if s.failInit != nil && s.failInit.Load() {
56 return errors.New("simulated validation error")
57 }
58 if len(s.cfg.Auth) == 0 {
59 return errors.New("auth is required")
60 }
61 s.published = &fakePublished{
62 blockOnCtx: s.blockOnCtx,
63 requireNonNilCtx: s.requireNonNilCtx,
64 }
65 return nil
66 }
67
68 type validateRaceStore struct {
69 cfg fakeConfig
70 initCount *atomic.Int32
71 validateStarted chan struct{}
72 validateRelease <-chan struct{}
73 published secretstore.PublishedStore
74 }
75
76 func (s *validateRaceStore) Configuration() any { return &s.cfg }
77 func (s *validateRaceStore) Publish() secretstore.PublishedStore {
78 return s.published
79 }
80
81 func (s *validateRaceStore) Init(context.Context) error {
82 if s.initCount != nil && s.initCount.Add(1) == 2 {
83 close(s.validateStarted)
84 <-s.validateRelease
85 }
86 if len(s.cfg.Auth) == 0 {
87 return errors.New("auth is required")
88 }
89 s.published = &fakePublished{}
90 return nil
91 }
92
93 func newFakeCreator(kind secretstore.StoreKind, failInit, blockOnCtx *atomic.Bool) secretstore.Creator {
94 return newFakeCreatorWithCtxProbe(kind, failInit, blockOnCtx, nil)
95 }
96
97 func newFakeCreatorWithCtxProbe(kind secretstore.StoreKind, failInit, blockOnCtx, requireNonNilCtx *atomic.Bool) secretstore.Creator {
98 schema := map[string]any{
99 "jsonSchema": map[string]any{
100 "type": "object",
101 "properties": map[string]any{
102 "auth": map[string]any{"type": "object"},
103 },
104 "required": []any{"auth"},
105 },
106 "uiSchema": map[string]any{},
107 }
108 bs, err := json.Marshal(schema)
109 if err != nil {
110 panic(err)
111 }
112
113 return secretstore.Creator{
114 Kind: kind,
115 DisplayName: "Fake Provider",
116 Schema: string(bs),
117 Create: func() secretstore.Store {
118 return &fakeStore{
119 failInit: failInit,
120 blockOnCtx: blockOnCtx,
121 requireNonNilCtx: requireNonNilCtx,
122 }
123 },
124 }
125 }
126
127 func newValidateRaceCreator(kind secretstore.StoreKind, validateStarted chan struct{}, validateRelease <-chan struct{}) secretstore.Creator {
128 schema := map[string]any{
129 "jsonSchema": map[string]any{
130 "type": "object",
131 "properties": map[string]any{
132 "auth": map[string]any{"type": "object"},
133 },
134 "required": []any{"auth"},
135 },
136 "uiSchema": map[string]any{},
137 }
138 bs, err := json.Marshal(schema)
139 if err != nil {
140 panic(err)
141 }
142
143 var initCount atomic.Int32
144 return secretstore.Creator{
145 Kind: kind,
146 DisplayName: "Fake Provider",
147 Schema: string(bs),
148 Create: func() secretstore.Store {
149 return &validateRaceStore{
150 initCount: &initCount,
151 validateStarted: validateStarted,
152 validateRelease: validateRelease,
153 }
154 },
155 }
156 }
157
158 func newFakeStore(_ *testing.T, _ secretstore.Service, kind secretstore.StoreKind, cfg fakeConfig, name string) secretstore.Config {
159 bs, err := json.Marshal(cfg)
160 if err != nil {
161 panic(err)
162 }
163 var payload map[string]any
164 if err := json.Unmarshal(bs, &payload); err != nil {
165 panic(err)
166 }
167 out := secretstore.Config(payload)
168 out.SetName(name)
169 out.SetKind(kind)
170 out.SetSource("dyncfg")
171 out.SetSourceType("dyncfg")
172 return out
173 }
174
175 func TestServiceStatusLifecycle(t *testing.T) {
176 var failInit atomic.Bool
177 svc := secretstore.NewService(newFakeCreator(secretstore.KindVault, &failInit, nil))
178
179 store := newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
180 Auth: map[string]any{"mode": "token_env"},
181 }, "vault_prod")
182
183 err := svc.Add(context.Background(), store)
184 require.NoError(t, err)
185
186 failInit.Store(true)
187 storeKey := secretstore.StoreKey(secretstore.KindVault, "vault_prod")
188 err = svc.ValidateStored(context.Background(), storeKey)
189 require.Error(t, err)
190
191 status, ok := svc.GetStatus(storeKey)
192 require.True(t, ok)
193 require.NotNil(t, status.LastValidation)
194 assert.False(t, status.LastValidation.OK)
195 assert.Equal(t, "simulated validation error", status.LastErrorSummary)
196
197 failInit.Store(false)
198 err = svc.ValidateStored(context.Background(), storeKey)
199 require.NoError(t, err)
200
201 status, ok = svc.GetStatus(storeKey)
202 require.True(t, ok)
203 require.NotNil(t, status.LastValidation)
204 assert.True(t, status.LastValidation.OK)
205 assert.Empty(t, status.LastErrorSummary)
206 }
207
208 func TestServiceResolveHonorsCanceledContext(t *testing.T) {
209 var blockOnCtx atomic.Bool
210 blockOnCtx.Store(true)
211
212 svc := secretstore.NewService(newFakeCreator(secretstore.KindVault, nil, &blockOnCtx))
213 store := newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
214 Auth: map[string]any{"mode": "token_env"},
215 }, "vault_prod")
216 err := svc.Add(context.Background(), store)
217 require.NoError(t, err)
218
219 ctx, cancel := context.WithCancel(context.Background())
220 cancel()
221
222 _, err = svc.Resolve(ctx, svc.Capture(), "vault:vault_prod:secret", "${store:vault:vault_prod:secret}")
223 require.ErrorIs(t, err, context.Canceled)
224 }
225
226 func TestServiceResolve_NormalizesNilContext(t *testing.T) {
227 var requireNonNilCtx atomic.Bool
228
229 svc := secretstore.NewService(newFakeCreatorWithCtxProbe(secretstore.KindVault, nil, nil, &requireNonNilCtx))
230 store := newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
231 Auth: map[string]any{"mode": "token_env"},
232 }, "vault_prod")
233 err := svc.Add(context.Background(), store)
234 require.NoError(t, err)
235
236 val, err := svc.Resolve(nil, svc.Capture(), "vault:vault_prod:secret/data/app#key", "${store:vault:vault_prod:secret/data/app#key}")
237 require.NoError(t, err)
238 assert.Equal(t, "secret/data/app#key", val)
239 assert.True(t, requireNonNilCtx.Load())
240 }
241
242 func TestServiceConcurrentResolveAndMutation(t *testing.T) {
243 svc := secretstore.NewService(newFakeCreator(secretstore.KindVault, nil, nil))
244 baseCfg := fakeConfig{
245 Auth: map[string]any{"mode": "token_env"},
246 }
247
248 err := svc.Add(context.Background(), newFakeStore(t, svc, secretstore.KindVault, baseCfg, "vault_prod"))
249 require.NoError(t, err)
250
251 var wg sync.WaitGroup
252 errCh := make(chan error, 32)
253
254 wg.Go(func() {
255 for range 100 {
256 snapshot := svc.Capture()
257 val, err := svc.Resolve(context.Background(), snapshot, "vault:vault_prod:secret/data/app#key", "${store:vault:vault_prod:secret/data/app#key}")
258 if err != nil {
259 errCh <- err
260 return
261 }
262 if val != "secret/data/app#key" {
263 errCh <- errors.New("unexpected resolved value")
264 return
265 }
266 }
267 })
268
269 wg.Go(func() {
270 for i := range 100 {
271 updateCfg := baseCfg
272 if i%2 == 0 {
273 updateCfg.Auth = map[string]any{
274 "mode": "token_env",
275 "tag": "alt",
276 }
277 }
278 if err := svc.Update(context.Background(), secretstore.StoreKey(secretstore.KindVault, "vault_prod"), newFakeStore(t, svc, secretstore.KindVault, updateCfg, "vault_prod")); err != nil {
279 errCh <- err
280 return
281 }
282 }
283 })
284
285 wg.Wait()
286 close(errCh)
287
288 for err := range errCh {
289 require.NoError(t, err)
290 }
291 }
292
293 func TestServiceValidateStored_RemovedDuringValidationReturnsNotFound(t *testing.T) {
294 validateStarted := make(chan struct{})
295 validateRelease := make(chan struct{})
296
297 svc := secretstore.NewService(newValidateRaceCreator(secretstore.KindVault, validateStarted, validateRelease))
298 store := newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
299 Auth: map[string]any{"mode": "token_env"},
300 }, "vault_prod")
301 require.NoError(t, svc.Add(context.Background(), store))
302
303 storeKey := secretstore.StoreKey(secretstore.KindVault, "vault_prod")
304 errCh := make(chan error, 1)
305
306 go func() {
307 errCh <- svc.ValidateStored(context.Background(), storeKey)
308 }()
309
310 <-validateStarted
311 require.NoError(t, svc.Remove(storeKey))
312 close(validateRelease)
313
314 err := <-errCh
315 require.Error(t, err)
316 assert.ErrorIs(t, err, secretstore.ErrStoreNotFound)
317 }
318
319 func TestServiceValidateStored_UpdatedDuringValidationReturnsRetryWithoutOverwritingStatus(t *testing.T) {
320 validateStarted := make(chan struct{})
321 validateRelease := make(chan struct{})
322
323 svc := secretstore.NewService(newValidateRaceCreator(secretstore.KindVault, validateStarted, validateRelease))
324 store := newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
325 Auth: map[string]any{"mode": "token_env"},
326 }, "vault_prod")
327 require.NoError(t, svc.Add(context.Background(), store))
328
329 storeKey := secretstore.StoreKey(secretstore.KindVault, "vault_prod")
330 errCh := make(chan error, 1)
331
332 go func() {
333 errCh <- svc.ValidateStored(context.Background(), storeKey)
334 }()
335
336 <-validateStarted
337 require.NoError(t, svc.Update(context.Background(), storeKey, newFakeStore(t, svc, secretstore.KindVault, fakeConfig{
338 Auth: map[string]any{"mode": "token_env", "tag": "new"},
339 }, "vault_prod")))
340 close(validateRelease)
341
342 err := <-errCh
343 require.Error(t, err)
344 assert.Contains(t, err.Error(), "changed during validation")
345
346 status, ok := svc.GetStatus(storeKey)
347 require.True(t, ok)
348 assert.Nil(t, status.LastValidation)
349 assert.Empty(t, status.LastErrorSummary)
350 }