1 package thinclient
2
3 import (
4 "context"
5 "io/fs"
6 "testing"
7
8 "github.com/go-logr/logr"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/mock"
11 corev1 "k8s.io/api/core/v1"
12 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13 kruntime "k8s.io/apimachinery/pkg/runtime"
14 utilruntime "k8s.io/apimachinery/pkg/util/runtime"
15 clientgoscheme "k8s.io/client-go/kubernetes/scheme"
16
17 ctrl "sigs.k8s.io/controller-runtime"
18 "sigs.k8s.io/controller-runtime/pkg/client"
19 "sigs.k8s.io/controller-runtime/pkg/client/fake"
20
21 "edge-infra.dev/pkg/sds"
22 v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
23 "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config"
24 fakeConfig "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config/fake"
25 "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient/configobject"
26 "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient/selector"
27 fakeFile "edge-infra.dev/pkg/sds/lib/os/file/fake"
28 passthrough "edge-infra.dev/pkg/sds/lib/os/passthrough/fake"
29 )
30
31 var (
32 cmName = "watch-kube"
33 testThinclientConfiguration = &corev1.ConfigMap{
34 ObjectMeta: metav1.ObjectMeta{
35 Name: cmName,
36 Namespace: sds.Namespace,
37 },
38 Data: map[string]string{"config.json": testThinclientConfigurationJSON},
39 }
40 testThinclientConfigurationNoSelectors = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: sds.Namespace}, Data: map[string]string{"config.json": "{\"selectors\":[]}"}}
41 testThinclientConfigurationInvalidJSON = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: sds.Namespace}, Data: map[string]string{"config.json": "!!!"}}
42 testThinclientConfigurationJSON = `{"selectors":[{"type":"ConfigMap","namespace":"examples","field":"metadata.name=exampleConfigMap","selector":"configmap.to.write=true","directory":"/tmp","service":""},{"type":"","namespace":"another-namespace","field":"metadata.name=exampleConfigMapMultipleLabels","selector":"configmap.to.write=true,another.label=value","directory":"/etc","service":"cfg-service"},{"type":"Secret","namespace":"examples","field":"metadata.name=exampleSecret","selector":"secret.to.write=true","directory":"/tmp","service":"secret-service"}]}`
43
44 testSelectors = selector.Selectors{testConfigMapSelector, testMultipleLabelsSelectorConfigMap, testSecretSelector}
45 testSecretSelectors = selector.Selectors{testSecretSelector}
46
47 testConfigMapSelector = selector.Selector{
48 Type: "ConfigMap",
49 Namespace: "examples",
50 Fields: "metadata.name=exampleConfigMap",
51 Labels: "configmap.to.write=true",
52 Directory: "/tmp",
53 Service: "",
54 }
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71 testMultipleLabelsSelectorConfigMap = selector.Selector{
72 Type: "ConfigMap",
73 Namespace: "another-namespace",
74 Fields: "metadata.name=exampleConfigMapMultipleLabels",
75 Labels: "configmap.to.write=true,another.label=value",
76 Directory: "/etc",
77 Service: "cfg-service",
78 }
79 testSecretSelector = selector.Selector{
80 Type: "Secret",
81 Namespace: "examples",
82 Fields: "metadata.name=exampleSecret",
83 Labels: "secret.to.write=true",
84 Directory: "/tmp",
85 Service: "secret-service",
86 }
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103 testNodeSelector = selector.Selector{
104 Type: "Node",
105 Namespace: "examples",
106 Fields: "metadata.name=exampleDeployment",
107 Labels: "deployment.to.write=false",
108 Directory: "/tmp",
109 Service: "",
110 }
111
112 testConfigMap = corev1.ConfigMap{
113 ObjectMeta: metav1.ObjectMeta{
114 Name: "exampleConfigMap",
115 Namespace: "examples",
116 Labels: map[string]string{"configmap.to.write": "true", "non.selector.label": "example"},
117 ResourceVersion: "999",
118 },
119 Data: testConfigMapData,
120 }
121 testConfigMapMultipleLabels = corev1.ConfigMap{
122 ObjectMeta: metav1.ObjectMeta{
123 Name: "exampleConfigMapMultipleLabels",
124 Namespace: "another-namespace",
125 Labels: map[string]string{"configmap.to.write": "true", "another.label": "value", "non.selector.label": "example"},
126 },
127 Data: testConfigMapData,
128 }
129 testSecret = corev1.Secret{
130 ObjectMeta: metav1.ObjectMeta{
131 Name: "exampleSecret",
132 Namespace: "examples",
133 Labels: map[string]string{"secret.to.write": "true", "non.selector.label": "example"},
134 ResourceVersion: "999",
135 },
136 Type: corev1.SecretType("kubernetes.io/tls"),
137 Data: testSecretData,
138 }
139
140 testConfigMapData = map[string]string{"example-cfg.json": `"example": "data"`, "example-cfg.yaml": "example: data", "example-cfg.txt": "example data"}
141 testSecretData = map[string][]byte{"example-secret.json": []byte(`"example": "data"`), "example-secret.yaml": []byte("example: data"), "example-secret.txt": []byte("example data")}
142
143 fileMode = fs.FileMode(0644)
144
145 expectedError = assert.AnError
146 )
147
148 func getFakeClientWithObjects(initObjs ...client.Object) client.Client {
149 fakeClient := fake.NewClientBuilder().WithScheme(createScheme())
150 fakeClient.WithIndex(&corev1.Secret{}, "type", getSecretTypeIndexerFunc())
151 fakeClient.WithIndex(&corev1.ConfigMap{}, "metadata.name", getNameTypeIndexerFunc())
152 fakeClient.WithIndex(&corev1.Secret{}, "metadata.name", getNameTypeIndexerFunc())
153 fakeClient.WithObjects(initObjs...)
154 return fakeClient.Build()
155 }
156
157 func getSecretTypeIndexerFunc() func(obj client.Object) []string {
158 return func(obj client.Object) []string {
159 secret, _ := obj.(*corev1.Secret)
160 return []string{string(secret.Type)}
161 }
162 }
163
164 func getNameTypeIndexerFunc() func(obj client.Object) []string {
165 return func(obj client.Object) []string {
166 return []string{obj.GetName()}
167 }
168 }
169
170
171 func createScheme() *kruntime.Scheme {
172 scheme := kruntime.NewScheme()
173 utilruntime.Must(clientgoscheme.AddToScheme(scheme))
174 utilruntime.Must(v1ien.AddToScheme(scheme))
175 return scheme
176 }
177
178 func TestNonConfigurationConfigMapReturnsNil(t *testing.T) {
179 plugin := &ConfigurationPlugin{}
180 kclient := getFakeClientWithObjects()
181 cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName})
182
183 ctx := context.Background()
184 ctrl.LoggerInto(ctx, logr.Logger{})
185
186 assert.Nil(t, plugin.Reconcile(ctx, &corev1.ConfigMap{}, cfg))
187 }
188
189
190 func TestUpdateTargetObjects(t *testing.T) {
191 ctx := context.Background()
192 fakeClient := getFakeClientWithObjects(&testConfigMap, &testConfigMapMultipleLabels, &testSecret)
193 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
194 fakeFileHandler, fileMock := fakeFile.NewFake()
195 exec := passthrough.New("done", 0)
196
197
198 fileMock.On("Exists", "/tmp/example-cfg.json").Return(true).Once()
199 fileMock.On("Read", "/tmp/example-cfg.json").Return("old data", nil).Once()
200 fileMock.On("Write", "/tmp/example-cfg.json", []byte(`"example": "data"`), fileMode).Return(nil).Once()
201 fileMock.On("Exists", "/tmp/example-cfg.txt").Return(false).Once()
202 fileMock.On("Write", "/tmp/example-cfg.txt", []byte("example data"), fileMode).Return(nil).Once()
203 fileMock.On("Exists", "/tmp/example-cfg.yaml").Return(true).Once()
204 fileMock.On("Read", "/tmp/example-cfg.yaml").Return("old data", nil).Once()
205 fileMock.On("Write", "/tmp/example-cfg.yaml", []byte("example: data"), fileMode).Return(nil).Once()
206
207
208 fileMock.On("Exists", "/etc/example-cfg.json").Return(true).Once()
209 fileMock.On("Read", "/etc/example-cfg.json").Return("old data", nil).Once()
210 fileMock.On("Write", "/etc/example-cfg.json", []byte(`"example": "data"`), fileMode).Return(nil).Once()
211 fileMock.On("Exists", "/etc/example-cfg.txt").Return(true).Once()
212 fileMock.On("Read", "/etc/example-cfg.txt").Return("old data", nil).Once()
213 fileMock.On("Write", "/etc/example-cfg.txt", []byte("example data"), fileMode).Return(nil).Once()
214 fileMock.On("Exists", "/etc/example-cfg.yaml").Return(true).Once()
215 fileMock.On("Read", "/etc/example-cfg.yaml").Return("old data", nil).Once()
216 fileMock.On("Write", "/etc/example-cfg.yaml", []byte("example: data"), fileMode).Return(nil).Once()
217
218
219 fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once()
220 fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once()
221 fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(nil).Once()
222 fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once()
223 fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once()
224 fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(nil).Once()
225 fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once()
226 fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once()
227
228 err := updateTargetObjects(ctx, testSelectors, fakeCfg, fakeFileHandler, exec)
229 assert.NoError(t, err)
230 fileMock.AssertNotCalled(t, "Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode)
231 fileMock.AssertExpectations(t)
232 }
233
234 func TestFailingToReadFromFileReturnsExpectedError(t *testing.T) {
235 ctx := context.Background()
236 fakeClient := getFakeClientWithObjects(&testSecret)
237 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
238 fakeFileHandler, fileMock := fakeFile.NewFake()
239 exec := passthrough.New("done", 0)
240
241 fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once()
242 fileMock.On("Read", "/tmp/example-secret.json").Return("", expectedError).Once()
243 fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once()
244 fileMock.On("Read", "/tmp/example-secret.txt").Return("", expectedError).Once()
245 fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once()
246 fileMock.On("Read", "/tmp/example-secret.yaml").Return("", expectedError).Once()
247 err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec)
248 fileMock.AssertNotCalled(t, "Write", mock.Anything, mock.Anything, mock.Anything)
249 assert.Equal(t, expectedError, err)
250 }
251
252 func TestFailingToWriteToFileReturnsExpectedError(t *testing.T) {
253 ctx := context.Background()
254 fakeClient := getFakeClientWithObjects(&testSecret)
255 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
256 fakeFileHandler, fileMock := fakeFile.NewFake()
257 exec := passthrough.New("done", 0)
258
259 fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once()
260 fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once()
261 fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(expectedError).Once()
262 fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once()
263 fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once()
264 fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(expectedError).Once()
265 fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once()
266 fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once()
267 fileMock.On("Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode).Return(expectedError).Once()
268
269 err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec)
270 assert.Equal(t, expectedError, err)
271 }
272
273 func TestFailingToRestartServiceReturnsExpectedError(t *testing.T) {
274 ctx := context.Background()
275 fakeClient := getFakeClientWithObjects(&testSecret)
276 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
277 fakeFileHandler, fileMock := fakeFile.NewFake()
278 exec := passthrough.New("done", 0)
279 exec.Command("/usr/bin/systemctl", "restart", "secret-service").Return(expectedError.Error(), 1)
280
281 fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once()
282 fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once()
283 fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(nil).Once()
284 fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once()
285 fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once()
286 fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(nil).Once()
287 fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once()
288 fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once()
289 fileMock.On("Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode).Return(nil).Once()
290
291 err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec)
292 assert.Equal(t, expectedError, err)
293 }
294
295 func TestErrorHandlingWhenUpdatingTargetObjects(t *testing.T) {
296 ctx := context.Background()
297 fakeClient := getFakeClientWithObjects()
298 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
299 fakeFileHandler, _ := fakeFile.NewFake()
300 exec := passthrough.New("done", 0)
301 err := updateTargetObjects(ctx, selector.Selectors{testNodeSelector}, fakeCfg, fakeFileHandler, exec)
302 assert.Error(t, err)
303 }
304
305 func TestNoMatchingSelectorsForWatcherPluginsReturnsNil(t *testing.T) {
306 log := logr.Discard()
307 ctx := context.Background()
308 ctrl.LoggerInto(ctx, log)
309 kclient := getFakeClientWithObjects(testThinclientConfiguration)
310 cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName})
311 assert.Nil(t, ConfigMapWatcherPlugin{}.Reconcile(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "testing", Name: "test"}}, cfg))
312 assert.Nil(t, SecretWatcherPlugin{}.Reconcile(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "testing", Name: "test"}}, cfg))
313 }
314
315 func TestGetSelectorForConfigObject(t *testing.T) {
316 ctx := context.Background()
317 tests := []struct {
318 inputThinclientConfigMap *corev1.ConfigMap
319 inputConfigObject configobject.ConfigObject
320 expectedSelector *selector.Selector
321 }{
322 {testThinclientConfigurationNoSelectors, configobject.NewFromConfigMap(testConfigMap), nil},
323 {testThinclientConfigurationNoSelectors, configobject.NewFromConfigMap(testConfigMap), nil},
324 {testThinclientConfiguration, configobject.NewFromConfigMap(testConfigMap), &testConfigMapSelector},
325 {testThinclientConfiguration, configobject.NewFromSecret(testSecret), &testSecretSelector},
326 }
327 for _, test := range tests {
328 kclient := getFakeClientWithObjects(test.inputThinclientConfigMap)
329 cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName})
330 selector, err := getSelectorForConfigObject(ctx, test.inputConfigObject, cfg)
331 assert.NoError(t, err)
332 assert.Equal(t, test.expectedSelector, selector)
333 }
334 }
335
336 func TestErrorHandlingWhenGettingSelectorForConfigObject(t *testing.T) {
337 ctx := context.Background()
338 tests := []struct {
339 inputThinclientConfigMap *corev1.ConfigMap
340 inputConfigObject configobject.ConfigObject
341 }{
342
343 {&corev1.ConfigMap{}, configobject.NewFromConfigMap(testConfigMap)},
344 {&corev1.ConfigMap{}, configobject.NewFromSecret(testSecret)},
345
346 {testThinclientConfigurationInvalidJSON, configobject.NewFromConfigMap(testConfigMap)},
347 {testThinclientConfigurationInvalidJSON, configobject.NewFromSecret(testSecret)},
348 }
349 for _, test := range tests {
350 fakeClient := getFakeClientWithObjects(test.inputThinclientConfigMap)
351 fakeCfg, _ := fakeConfig.NewFake(fakeClient)
352 selector, err := getSelectorForConfigObject(ctx, test.inputConfigObject, fakeCfg)
353 assert.Error(t, err)
354 assert.Nil(t, selector)
355 }
356 }
357
View as plain text