master
go 197 lines 5.75 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package jobmgr
4
5 import (
6 "bytes"
7 "context"
8 "maps"
9 "testing"
10
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
15 "github.com/netdata/netdata/go/plugins/pkg/safewriter"
16 "github.com/netdata/netdata/go/plugins/plugin/agent/secrets/secretstore"
17 "github.com/netdata/netdata/go/plugins/plugin/framework/dyncfg"
18 "github.com/netdata/netdata/go/plugins/plugin/framework/functions"
19 )
20
21 func TestExtractSecretStoreKeys(t *testing.T) {
22 tests := map[string]struct {
23 cfg map[string]any
24 want []string
25 }{
26 "single store reference": {
27 cfg: map[string]any{
28 "password": "${store:aws-sm:aws_prod:db/password}",
29 },
30 want: []string{"aws-sm:aws_prod"},
31 },
32 "multiple refs with non-store refs ignored": {
33 cfg: map[string]any{
34 "dsn": "postgres://${env:USER}:${store:vault:vault_prod:secret/data/db#password}@host/${store:aws-sm:aws_prod:db#pw}",
35 },
36 want: []string{"aws-sm:aws_prod", "vault:vault_prod"},
37 },
38 "nested structures and internal keys": {
39 cfg: map[string]any{
40 "__source__": "${store:vault:ignored:foo}",
41 "outer": map[string]any{
42 "items": []any{
43 "${store:gcp-sm:gcp_prod:project/secret/latest}",
44 map[any]any{"x": "${store:azure-kv:az_prod:vault/secret}"},
45 },
46 },
47 },
48 want: []string{"azure-kv:az_prod", "gcp-sm:gcp_prod"},
49 },
50 "invalid store refs are ignored": {
51 cfg: map[string]any{
52 "a": "${store::bad:secret}",
53 "b": "${store:bad-id:thing:path}",
54 "c": "${store:vault:good_1:path}",
55 },
56 want: []string{"vault:good_1"},
57 },
58 }
59
60 for name, tc := range tests {
61 t.Run(name, func(t *testing.T) {
62 cfg := prepareUserCfg("mod", "job")
63 maps.Copy(cfg, tc.cfg)
64 got := extractSecretStoreKeys(cfg)
65 assert.Equal(t, tc.want, got)
66 })
67 }
68 }
69
70 func TestSecretStoreDepsImpacted(t *testing.T) {
71 tests := map[string]struct {
72 run func(d *secretStoreDeps)
73 storeKey string
74 wantExposed []secretstore.JobRef
75 wantRunning []secretstore.JobRef
76 }{
77 "set active and running": {
78 run: func(d *secretStoreDeps) {
79 d.SetActiveJobStores("mysql_prod", "mysql:prod", []string{"vault:vault_prod"})
80 d.setRunning("mysql_prod", true)
81 },
82 storeKey: "vault:vault_prod",
83 wantExposed: []secretstore.JobRef{
84 {ID: "mysql_prod", Display: "mysql:prod"},
85 },
86 wantRunning: []secretstore.JobRef{
87 {ID: "mysql_prod", Display: "mysql:prod"},
88 },
89 },
90 "replace active stores keeps running alignment": {
91 run: func(d *secretStoreDeps) {
92 d.SetActiveJobStores("mysql_prod", "mysql:prod", []string{"vault:vault_prod"})
93 d.setRunning("mysql_prod", true)
94 d.SetActiveJobStores("mysql_prod", "mysql:prod", []string{"aws-sm:aws_prod"})
95 },
96 storeKey: "aws-sm:aws_prod",
97 wantExposed: []secretstore.JobRef{
98 {ID: "mysql_prod", Display: "mysql:prod"},
99 },
100 wantRunning: []secretstore.JobRef{
101 {ID: "mysql_prod", Display: "mysql:prod"},
102 },
103 },
104 "remove active job clears impacted": {
105 run: func(d *secretStoreDeps) {
106 d.SetActiveJobStores("mysql_prod", "mysql:prod", []string{"vault:vault_prod"})
107 d.setRunning("mysql_prod", true)
108 d.RemoveActiveJob("mysql_prod")
109 },
110 storeKey: "vault:vault_prod",
111 wantExposed: nil,
112 wantRunning: nil,
113 },
114 "non-running job appears only in exposed": {
115 run: func(d *secretStoreDeps) {
116 d.SetActiveJobStores("redis_prod", "redis:prod", []string{"gcp-sm:gcp_prod"})
117 d.setRunning("redis_prod", false)
118 },
119 storeKey: "gcp-sm:gcp_prod",
120 wantExposed: []secretstore.JobRef{
121 {ID: "redis_prod", Display: "redis:prod"},
122 },
123 wantRunning: nil,
124 },
125 }
126
127 for name, tc := range tests {
128 t.Run(name, func(t *testing.T) {
129 deps := newSecretStoreDeps()
130 tc.run(deps)
131
132 exposed, running := deps.Impacted(tc.storeKey)
133 assert.Equal(t, tc.wantExposed, exposed)
134 assert.Equal(t, tc.wantRunning, running)
135 })
136 }
137 }
138
139 func TestDyncfgTestDoesNotMutateSecretStoreDeps(t *testing.T) {
140 tests := map[string]struct{}{
141 "command test leaves dependency index unchanged": {},
142 }
143
144 for name := range tests {
145 t.Run(name, func(t *testing.T) {
146 var buf bytes.Buffer
147 mgr := newCollectorTestManager()
148 mgr.ctx = context.Background()
149 mgr.SetDyncfgResponder(dyncfg.NewResponder(netdataapi.New(safewriter.New(&buf))))
150
151 cfg := prepareDyncfgCfg("success", "job")
152 cfg["password"] = "${store:vault:vault_prod:secret/data/mysql#password}"
153 mgr.syncSecretStoreDepsForConfig(cfg)
154 mgr.secretStoreDeps.setRunning(cfg.FullName(), true)
155
156 beforeExposed, beforeRunning := mgr.secretStoreDeps.Impacted("vault:vault_prod")
157 require.Len(t, beforeExposed, 1)
158 require.Len(t, beforeRunning, 1)
159
160 fn := dyncfg.NewFunction(functions.Function{
161 UID: "secretstore-deps-test",
162 ContentType: "application/json",
163 Payload: mustMarshalCollectorConfigPayload(t, prepareDyncfgCfg("success", "job")),
164 Args: []string{mgr.dyncfgModID("success"), string(dyncfg.CommandTest), "job"},
165 })
166
167 mgr.dyncfgCollectorSeqExec(fn)
168 mgr.cmdTestWG.Wait()
169
170 afterExposed, afterRunning := mgr.secretStoreDeps.Impacted("vault:vault_prod")
171 assert.Equal(t, beforeExposed, afterExposed)
172 assert.Equal(t, beforeRunning, afterRunning)
173 })
174 }
175 }
176
177 func TestSecretStoreDepsNoStateLeakOnRemoveThenStop(t *testing.T) {
178 tests := map[string]struct{}{
179 "setRunning false on missing state is no-op": {},
180 }
181
182 for name := range tests {
183 t.Run(name, func(t *testing.T) {
184 deps := newSecretStoreDeps()
185
186 deps.SetActiveJobStores("mysql_prod", "mysql:prod", []string{"vault:vault_prod"})
187 deps.setRunning("mysql_prod", true)
188 deps.RemoveActiveJob("mysql_prod")
189 deps.setRunning("mysql_prod", false)
190
191 assert.Empty(t, deps.jobs)
192 exposed, running := deps.Impacted("vault:vault_prod")
193 assert.Empty(t, exposed)
194 assert.Empty(t, running)
195 })
196 }
197 }