...

Source file src/edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient/thinclient_test.go

Documentation: edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/plugins/thinclient

     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  	// testConfigMapSelectorNoFields = selector.Selector{
    56  	// 	Type:      "ConfigMap",
    57  	// 	Namespace: "examples",
    58  	// 	Fields:    "",
    59  	// 	Labels:    "configmap.to.write=true",
    60  	// 	Directory: "/tmp",
    61  	// 	Service:   "",
    62  	// }
    63  	// testConfigMapSelectorNoLabels = selector.Selector{
    64  	// 	Type:      "ConfigMap",
    65  	// 	Namespace: "examples",
    66  	// 	Fields:    "metadata.name=exampleConfigMap",
    67  	// 	Labels:    "",
    68  	// 	Directory: "/tmp",
    69  	// 	Service:   "",
    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  	// testConfigMapSelectorInvalidLabels = selector.Selector{
    88  	// 	Type:      "ConfigMap",
    89  	// 	Namespace: "examples",
    90  	// 	Fields:    "metadata.name=exampleConfigMap",
    91  	// 	Labels:    "!!!",
    92  	// 	Directory: "/tmp",
    93  	// 	Service:   "",
    94  	// }
    95  	// testSecretSelectorInvalidFields = selector.Selector{
    96  	// 	Type:      "Secret",
    97  	// 	Namespace: "examples",
    98  	// 	Fields:    "!!!",
    99  	// 	Labels:    "secret.to.write=true",
   100  	// 	Directory: "/tmp",
   101  	// 	Service:   "",
   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  // Creates scheme for the fake client
   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  // Tests config with three selectors of varying type
   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  	// ConfigMap selector with no service to restart and a file that doesn't exist
   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  	// ConfigMap selector with multiple labels and a service to restart
   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  	// Secret selector with service and one up-to-date file
   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() // data is up-to-date and not updated                // service is restarted
   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) // example-secret.yaml already up to date
   231  	fileMock.AssertExpectations(t)                                                                      // assert all mocks were exercised
   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() // failed to read from file
   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() // failed to write to file
   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},         // no ConfigMap selectors from empty thin client data field
   323  		{testThinclientConfigurationNoSelectors, configobject.NewFromConfigMap(testConfigMap), nil},         // no Secret selectors from empty thin client data field
   324  		{testThinclientConfiguration, configobject.NewFromConfigMap(testConfigMap), &testConfigMapSelector}, // ConfigMap selectors extracted from thin client ConfigMap as expected
   325  		{testThinclientConfiguration, configobject.NewFromSecret(testSecret), &testSecretSelector},          // Secret selectors extracted from thin client ConfigMap as expected
   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  		// error returned when thin client ConfigMap doesn't exist
   343  		{&corev1.ConfigMap{}, configobject.NewFromConfigMap(testConfigMap)},
   344  		{&corev1.ConfigMap{}, configobject.NewFromSecret(testSecret)},
   345  		// invalid JSON in thin client ConfigMap returns error
   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