master
go 219 lines 7.04 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package secretsctl
4
5 import (
6 "context"
7 "encoding/json"
8 "fmt"
9 "testing"
10
11 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
12 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
13 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16 )
17
18 const testPluginName = "go.d"
19
20 func TestSecretStoreCallbacks(t *testing.T) {
21 tests := map[string]struct {
22 run func(t *testing.T, cb *secretStoreCallbacks, svc secretstore.Service, restart *secretStoreCallbackRestartRecorder)
23 }{
24 "isolation without manager": {
25 run: func(t *testing.T, cb *secretStoreCallbacks, svc secretstore.Service, restart *secretStoreCallbackRestartRecorder) {
26 addFn := newSecretStoreCallbackFunction(t, "ss-add", testSecretStoreTemplateID(secretstore.KindVault), dyncfg.CommandAdd, "vault_prod", map[string]any{"value": "one"})
27 key, name, ok := cb.ExtractKey(addFn)
28 require.True(t, ok)
29 assert.Equal(t, secretstore.StoreKey(secretstore.KindVault, "vault_prod"), key)
30 assert.Equal(t, "vault_prod", name)
31
32 cfg, err := cb.ParseAndValidate(addFn, name)
33 require.NoError(t, err)
34 assert.Empty(t, restart.calls)
35 assert.Equal(t, "", cb.TakeCommandMessage())
36
37 require.NoError(t, cb.Start(cfg))
38 assert.Equal(t, []string{key}, restart.calls)
39 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
40 assert.Equal(t, testSecretStoreConfigID(key), cb.ConfigID(cfg))
41
42 status, ok := svc.GetStatus(key)
43 require.True(t, ok)
44 assert.Equal(t, secretstore.KindVault, status.Kind)
45 assert.Equal(t, "vault_prod", status.Name)
46 assert.Equal(t, "one", resolveTestStoreValue(t, svc, key))
47
48 updateFn := newSecretStoreCallbackFunction(t, "ss-update", testSecretStoreConfigID(key), dyncfg.CommandUpdate, "", map[string]any{"value": "two"})
49 updateKey, updateName, ok := cb.ExtractKey(updateFn)
50 require.True(t, ok)
51 assert.Equal(t, key, updateKey)
52 assert.Equal(t, name, updateName)
53
54 updatedCfg, err := cb.ParseAndValidate(updateFn, "")
55 require.NoError(t, err)
56 assert.Len(t, restart.calls, 1)
57
58 require.NoError(t, cb.Update(cfg, updatedCfg))
59 assert.Equal(t, []string{key, key}, restart.calls)
60 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
61 assert.Equal(t, "two", resolveTestStoreValue(t, svc, key))
62
63 cb.Stop(updatedCfg)
64 assert.Equal(t, []string{key, key, key}, restart.calls)
65 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
66 _, ok = svc.GetStatus(key)
67 assert.False(t, ok)
68 },
69 },
70 "restart seam called only on mutation": {
71 run: func(t *testing.T, cb *secretStoreCallbacks, _ secretstore.Service, restart *secretStoreCallbackRestartRecorder) {
72 addFn := newSecretStoreCallbackFunction(t, "ss-mutate-add", testSecretStoreTemplateID(secretstore.KindVault), dyncfg.CommandAdd, "vault_prod", map[string]any{"value": "one"})
73 key, name, ok := cb.ExtractKey(addFn)
74 require.True(t, ok)
75
76 cfg, err := cb.ParseAndValidate(addFn, name)
77 require.NoError(t, err)
78 assert.Empty(t, restart.calls)
79
80 assert.Equal(t, testSecretStoreConfigID(key), cb.ConfigID(cfg))
81 assert.Equal(t, "", cb.TakeCommandMessage())
82 cb.OnStatusChange(nil, dyncfg.StatusAccepted, addFn)
83 assert.Empty(t, restart.calls)
84
85 require.NoError(t, cb.Start(cfg))
86 assert.Equal(t, []string{key}, restart.calls)
87 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
88
89 updateFn := newSecretStoreCallbackFunction(t, "ss-mutate-update", testSecretStoreConfigID(key), dyncfg.CommandUpdate, "", map[string]any{"value": "two"})
90 updatedCfg, err := cb.ParseAndValidate(updateFn, "")
91 require.NoError(t, err)
92 assert.Len(t, restart.calls, 1)
93
94 require.NoError(t, cb.Update(cfg, updatedCfg))
95 assert.Equal(t, []string{key, key}, restart.calls)
96 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
97
98 cb.Stop(updatedCfg)
99 assert.Equal(t, []string{key, key, key}, restart.calls)
100 assert.Equal(t, "restart:"+key, cb.TakeCommandMessage())
101 },
102 },
103 }
104
105 for name, tc := range tests {
106 t.Run(name, func(t *testing.T) {
107 cb, svc, restart := newSecretStoreCallbacksTestSubject()
108 tc.run(t, cb, svc, restart)
109 })
110 }
111 }
112
113 type secretStoreCallbackRestartRecorder struct {
114 calls []string
115 }
116
117 func newSecretStoreCallbacksTestSubject() (*secretStoreCallbacks, secretstore.Service, *secretStoreCallbackRestartRecorder) {
118 svc := newTestSecretStoreService()
119 restart := &secretStoreCallbackRestartRecorder{}
120 cb := newSecretStoreCallbacks(secretStoreCallbackDeps{
121 pluginName: testPluginName,
122 service: svc,
123 restartDependentJobs: func(storeKey string) string {
124 restart.calls = append(restart.calls, storeKey)
125 return "restart:" + storeKey
126 },
127 })
128 return cb, svc, restart
129 }
130
131 func newSecretStoreCallbackFunction(t *testing.T, uid, id string, cmd dyncfg.Command, jobName string, payload any) dyncfg.Function {
132 t.Helper()
133
134 args := []string{id, string(cmd)}
135 if jobName != "" {
136 args = append(args, jobName)
137 }
138
139 fn := functions.Function{
140 UID: uid,
141 Args: args,
142 }
143 if payload != nil {
144 fn.ContentType = "application/json"
145 fn.Payload = mustJSON(t, payload)
146 }
147 return dyncfg.NewFunction(fn)
148 }
149
150 func testSecretStoreTemplateID(kind secretstore.StoreKind) string {
151 return fmt.Sprintf("%s%s", fmt.Sprintf(dyncfgSecretStorePrefixf, testPluginName), kind)
152 }
153
154 func testSecretStoreConfigID(key string) string {
155 return fmt.Sprintf("%s%s", fmt.Sprintf(dyncfgSecretStorePrefixf, testPluginName), key)
156 }
157
158 func resolveTestStoreValue(t *testing.T, svc secretstore.Service, key string) string {
159 t.Helper()
160
161 kind, name, err := secretstore.ParseStoreKey(key)
162 require.NoError(t, err)
163
164 ref := fmt.Sprintf("%s:%s:value", kind, name)
165 original := fmt.Sprintf("${store:%s}", ref)
166 value, err := svc.Resolve(context.Background(), svc.Capture(), ref, original)
167 require.NoError(t, err)
168 return value
169 }
170
171 type testStoreConfig struct {
172 Value string `yaml:"value" json:"value"`
173 }
174
175 type testStore struct {
176 testStoreConfig `yaml:",inline" json:""`
177 }
178
179 func (s *testStore) Configuration() any { return &s.testStoreConfig }
180
181 func (s *testStore) Init(context.Context) error {
182 if s.testStoreConfig.Value == "" {
183 return fmt.Errorf("value is required")
184 }
185 return nil
186 }
187
188 func (s *testStore) Publish() secretstore.PublishedStore {
189 return &testPublishedStore{value: s.testStoreConfig.Value}
190 }
191
192 type testPublishedStore struct {
193 value string
194 }
195
196 func (s *testPublishedStore) Resolve(_ context.Context, req secretstore.ResolveRequest) (string, error) {
197 if req.Operand != "value" {
198 return "", fmt.Errorf("unexpected operand %q", req.Operand)
199 }
200 return s.value, nil
201 }
202
203 func newTestSecretStoreService() secretstore.Service {
204 return secretstore.NewService(secretstore.Creator{
205 Kind: secretstore.KindVault,
206 DisplayName: "Vault",
207 Schema: `{"jsonSchema":{"type":"object","properties":{"value":{"type":"string"}}},"uiSchema":[]}`,
208 Create: func() secretstore.Store {
209 return &testStore{}
210 },
211 })
212 }
213
214 func mustJSON(t *testing.T, v any) []byte {
215 t.Helper()
216 bs, err := json.Marshal(v)
217 require.NoError(t, err)
218 return bs
219 }