package thinclient import ( "context" "io/fs" "testing" "github.com/go-logr/logr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" kruntime "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" "edge-infra.dev/pkg/sds" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config" fakeConfig "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config/fake" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient/configobject" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient/selector" fakeFile "edge-infra.dev/pkg/sds/lib/os/file/fake" passthrough "edge-infra.dev/pkg/sds/lib/os/passthrough/fake" ) var ( cmName = "watch-kube" testThinclientConfiguration = &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: cmName, Namespace: sds.Namespace, }, Data: map[string]string{"config.json": testThinclientConfigurationJSON}, } testThinclientConfigurationNoSelectors = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: sds.Namespace}, Data: map[string]string{"config.json": "{\"selectors\":[]}"}} testThinclientConfigurationInvalidJSON = &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: cmName, Namespace: sds.Namespace}, Data: map[string]string{"config.json": "!!!"}} 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"}]}` testSelectors = selector.Selectors{testConfigMapSelector, testMultipleLabelsSelectorConfigMap, testSecretSelector} testSecretSelectors = selector.Selectors{testSecretSelector} testConfigMapSelector = selector.Selector{ Type: "ConfigMap", Namespace: "examples", Fields: "metadata.name=exampleConfigMap", Labels: "configmap.to.write=true", Directory: "/tmp", Service: "", } // testConfigMapSelectorNoFields = selector.Selector{ // Type: "ConfigMap", // Namespace: "examples", // Fields: "", // Labels: "configmap.to.write=true", // Directory: "/tmp", // Service: "", // } // testConfigMapSelectorNoLabels = selector.Selector{ // Type: "ConfigMap", // Namespace: "examples", // Fields: "metadata.name=exampleConfigMap", // Labels: "", // Directory: "/tmp", // Service: "", // } testMultipleLabelsSelectorConfigMap = selector.Selector{ Type: "ConfigMap", Namespace: "another-namespace", Fields: "metadata.name=exampleConfigMapMultipleLabels", Labels: "configmap.to.write=true,another.label=value", Directory: "/etc", Service: "cfg-service", } testSecretSelector = selector.Selector{ Type: "Secret", Namespace: "examples", Fields: "metadata.name=exampleSecret", Labels: "secret.to.write=true", Directory: "/tmp", Service: "secret-service", } // testConfigMapSelectorInvalidLabels = selector.Selector{ // Type: "ConfigMap", // Namespace: "examples", // Fields: "metadata.name=exampleConfigMap", // Labels: "!!!", // Directory: "/tmp", // Service: "", // } // testSecretSelectorInvalidFields = selector.Selector{ // Type: "Secret", // Namespace: "examples", // Fields: "!!!", // Labels: "secret.to.write=true", // Directory: "/tmp", // Service: "", // } testNodeSelector = selector.Selector{ Type: "Node", Namespace: "examples", Fields: "metadata.name=exampleDeployment", Labels: "deployment.to.write=false", Directory: "/tmp", Service: "", } testConfigMap = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "exampleConfigMap", Namespace: "examples", Labels: map[string]string{"configmap.to.write": "true", "non.selector.label": "example"}, ResourceVersion: "999", }, Data: testConfigMapData, } testConfigMapMultipleLabels = corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "exampleConfigMapMultipleLabels", Namespace: "another-namespace", Labels: map[string]string{"configmap.to.write": "true", "another.label": "value", "non.selector.label": "example"}, }, Data: testConfigMapData, } testSecret = corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "exampleSecret", Namespace: "examples", Labels: map[string]string{"secret.to.write": "true", "non.selector.label": "example"}, ResourceVersion: "999", }, Type: corev1.SecretType("kubernetes.io/tls"), Data: testSecretData, } testConfigMapData = map[string]string{"example-cfg.json": `"example": "data"`, "example-cfg.yaml": "example: data", "example-cfg.txt": "example data"} testSecretData = map[string][]byte{"example-secret.json": []byte(`"example": "data"`), "example-secret.yaml": []byte("example: data"), "example-secret.txt": []byte("example data")} fileMode = fs.FileMode(0644) expectedError = assert.AnError ) func getFakeClientWithObjects(initObjs ...client.Object) client.Client { fakeClient := fake.NewClientBuilder().WithScheme(createScheme()) fakeClient.WithIndex(&corev1.Secret{}, "type", getSecretTypeIndexerFunc()) fakeClient.WithIndex(&corev1.ConfigMap{}, "metadata.name", getNameTypeIndexerFunc()) fakeClient.WithIndex(&corev1.Secret{}, "metadata.name", getNameTypeIndexerFunc()) fakeClient.WithObjects(initObjs...) return fakeClient.Build() } func getSecretTypeIndexerFunc() func(obj client.Object) []string { return func(obj client.Object) []string { secret, _ := obj.(*corev1.Secret) return []string{string(secret.Type)} } } func getNameTypeIndexerFunc() func(obj client.Object) []string { return func(obj client.Object) []string { return []string{obj.GetName()} } } // Creates scheme for the fake client func createScheme() *kruntime.Scheme { scheme := kruntime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(v1ien.AddToScheme(scheme)) return scheme } func TestNonConfigurationConfigMapReturnsNil(t *testing.T) { plugin := &ConfigurationPlugin{} kclient := getFakeClientWithObjects() cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName}) ctx := context.Background() ctrl.LoggerInto(ctx, logr.Logger{}) assert.Nil(t, plugin.Reconcile(ctx, &corev1.ConfigMap{}, cfg)) } // Tests config with three selectors of varying type func TestUpdateTargetObjects(t *testing.T) { ctx := context.Background() fakeClient := getFakeClientWithObjects(&testConfigMap, &testConfigMapMultipleLabels, &testSecret) fakeCfg, _ := fakeConfig.NewFake(fakeClient) fakeFileHandler, fileMock := fakeFile.NewFake() exec := passthrough.New("done", 0) // ConfigMap selector with no service to restart and a file that doesn't exist fileMock.On("Exists", "/tmp/example-cfg.json").Return(true).Once() fileMock.On("Read", "/tmp/example-cfg.json").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-cfg.json", []byte(`"example": "data"`), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-cfg.txt").Return(false).Once() fileMock.On("Write", "/tmp/example-cfg.txt", []byte("example data"), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-cfg.yaml").Return(true).Once() fileMock.On("Read", "/tmp/example-cfg.yaml").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-cfg.yaml", []byte("example: data"), fileMode).Return(nil).Once() // ConfigMap selector with multiple labels and a service to restart fileMock.On("Exists", "/etc/example-cfg.json").Return(true).Once() fileMock.On("Read", "/etc/example-cfg.json").Return("old data", nil).Once() fileMock.On("Write", "/etc/example-cfg.json", []byte(`"example": "data"`), fileMode).Return(nil).Once() fileMock.On("Exists", "/etc/example-cfg.txt").Return(true).Once() fileMock.On("Read", "/etc/example-cfg.txt").Return("old data", nil).Once() fileMock.On("Write", "/etc/example-cfg.txt", []byte("example data"), fileMode).Return(nil).Once() fileMock.On("Exists", "/etc/example-cfg.yaml").Return(true).Once() fileMock.On("Read", "/etc/example-cfg.yaml").Return("old data", nil).Once() fileMock.On("Write", "/etc/example-cfg.yaml", []byte("example: data"), fileMode).Return(nil).Once() // Secret selector with service and one up-to-date file fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once() // data is up-to-date and not updated // service is restarted err := updateTargetObjects(ctx, testSelectors, fakeCfg, fakeFileHandler, exec) assert.NoError(t, err) fileMock.AssertNotCalled(t, "Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode) // example-secret.yaml already up to date fileMock.AssertExpectations(t) // assert all mocks were exercised } func TestFailingToReadFromFileReturnsExpectedError(t *testing.T) { ctx := context.Background() fakeClient := getFakeClientWithObjects(&testSecret) fakeCfg, _ := fakeConfig.NewFake(fakeClient) fakeFileHandler, fileMock := fakeFile.NewFake() exec := passthrough.New("done", 0) fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.json").Return("", expectedError).Once() // failed to read from file fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.txt").Return("", expectedError).Once() fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.yaml").Return("", expectedError).Once() err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec) fileMock.AssertNotCalled(t, "Write", mock.Anything, mock.Anything, mock.Anything) assert.Equal(t, expectedError, err) } func TestFailingToWriteToFileReturnsExpectedError(t *testing.T) { ctx := context.Background() fakeClient := getFakeClientWithObjects(&testSecret) fakeCfg, _ := fakeConfig.NewFake(fakeClient) fakeFileHandler, fileMock := fakeFile.NewFake() exec := passthrough.New("done", 0) fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(expectedError).Once() // failed to write to file fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(expectedError).Once() fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once() fileMock.On("Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode).Return(expectedError).Once() err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec) assert.Equal(t, expectedError, err) } func TestFailingToRestartServiceReturnsExpectedError(t *testing.T) { ctx := context.Background() fakeClient := getFakeClientWithObjects(&testSecret) fakeCfg, _ := fakeConfig.NewFake(fakeClient) fakeFileHandler, fileMock := fakeFile.NewFake() exec := passthrough.New("done", 0) exec.Command("/usr/bin/systemctl", "restart", "secret-service").Return(expectedError.Error(), 1) fileMock.On("Exists", "/tmp/example-secret.json").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.json").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.json", []byte(`"example": "data"`), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-secret.txt").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.txt").Return("old data", nil).Once() fileMock.On("Write", "/tmp/example-secret.txt", []byte("example data"), fileMode).Return(nil).Once() fileMock.On("Exists", "/tmp/example-secret.yaml").Return(true).Once() fileMock.On("Read", "/tmp/example-secret.yaml").Return("example: data", nil).Once() fileMock.On("Write", "/tmp/example-secret.yaml", []byte("example: data"), fileMode).Return(nil).Once() err := updateTargetObjects(ctx, testSecretSelectors, fakeCfg, fakeFileHandler, exec) assert.Equal(t, expectedError, err) } func TestErrorHandlingWhenUpdatingTargetObjects(t *testing.T) { ctx := context.Background() fakeClient := getFakeClientWithObjects() fakeCfg, _ := fakeConfig.NewFake(fakeClient) fakeFileHandler, _ := fakeFile.NewFake() exec := passthrough.New("done", 0) err := updateTargetObjects(ctx, selector.Selectors{testNodeSelector}, fakeCfg, fakeFileHandler, exec) assert.Error(t, err) } func TestNoMatchingSelectorsForWatcherPluginsReturnsNil(t *testing.T) { log := logr.Discard() ctx := context.Background() ctrl.LoggerInto(ctx, log) kclient := getFakeClientWithObjects(testThinclientConfiguration) cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName}) assert.Nil(t, ConfigMapWatcherPlugin{}.Reconcile(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Namespace: "testing", Name: "test"}}, cfg)) assert.Nil(t, SecretWatcherPlugin{}.Reconcile(ctx, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "testing", Name: "test"}}, cfg)) } func TestGetSelectorForConfigObject(t *testing.T) { ctx := context.Background() tests := []struct { inputThinclientConfigMap *corev1.ConfigMap inputConfigObject configobject.ConfigObject expectedSelector *selector.Selector }{ {testThinclientConfigurationNoSelectors, configobject.NewFromConfigMap(testConfigMap), nil}, // no ConfigMap selectors from empty thin client data field {testThinclientConfigurationNoSelectors, configobject.NewFromConfigMap(testConfigMap), nil}, // no Secret selectors from empty thin client data field {testThinclientConfiguration, configobject.NewFromConfigMap(testConfigMap), &testConfigMapSelector}, // ConfigMap selectors extracted from thin client ConfigMap as expected {testThinclientConfiguration, configobject.NewFromSecret(testSecret), &testSecretSelector}, // Secret selectors extracted from thin client ConfigMap as expected } for _, test := range tests { kclient := getFakeClientWithObjects(test.inputThinclientConfigMap) cfg := config.NewConfig(kclient, nil, nil, config.Flags{ThinclientConfigMap: &cmName}) selector, err := getSelectorForConfigObject(ctx, test.inputConfigObject, cfg) assert.NoError(t, err) assert.Equal(t, test.expectedSelector, selector) } } func TestErrorHandlingWhenGettingSelectorForConfigObject(t *testing.T) { ctx := context.Background() tests := []struct { inputThinclientConfigMap *corev1.ConfigMap inputConfigObject configobject.ConfigObject }{ // error returned when thin client ConfigMap doesn't exist {&corev1.ConfigMap{}, configobject.NewFromConfigMap(testConfigMap)}, {&corev1.ConfigMap{}, configobject.NewFromSecret(testSecret)}, // invalid JSON in thin client ConfigMap returns error {testThinclientConfigurationInvalidJSON, configobject.NewFromConfigMap(testConfigMap)}, {testThinclientConfigurationInvalidJSON, configobject.NewFromSecret(testSecret)}, } for _, test := range tests { fakeClient := getFakeClientWithObjects(test.inputThinclientConfigMap) fakeCfg, _ := fakeConfig.NewFake(fakeClient) selector, err := getSelectorForConfigObject(ctx, test.inputConfigObject, fakeCfg) assert.Error(t, err) assert.Nil(t, selector) } }