package remoteagentconfig import ( "context" "fmt" "io/fs" "os" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "edge-infra.dev/pkg/edge/info" v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1" "edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config" fakeFile "edge-infra.dev/pkg/sds/lib/os/file/fake" "edge-infra.dev/test/f2" ) var f f2.Framework var tplConfig = ` provider: "{{ .Provider }}" subscriptions: - name: "sub_{{ .BannerID }}_{{ .StoreID }}_{{ .TerminalID }}_dsds-ea-request bannerID: "{{ .BannerID }}" storeID: "{{ .StoreID }}" terminalID: "{{ .TerminalID }}" CredentialsPath: "{{ .CredentialsPath }}" handler: ResponseTopic: "topic_{{ .BannerID }}_dsds-ea-request" Type: "cli" ` var expConfig = ` provider: "my-provider" subscriptions: - name: "sub_my-banner-id_my-store-id_my-terminal-id_dsds-ea-request bannerID: "my-banner-id" storeID: "my-store-id" terminalID: "my-terminal-id" CredentialsPath: "%s/%s" handler: ResponseTopic: "topic_my-banner-id_dsds-ea-request" Type: "cli" ` //nolint:gosec // Secret template, not secret var adcKey = ` { "type": "service_account", "project_id": "project", "private_key_id": "id", "private_key": "-----BEGIN PRIVATE KEY-----\nKey Data\n-----END PRIVATE KEY-----\n", "client_email": "sa-id@project.iam.gserviceaccount.com", "client_id": "id", "auth_uri": "https://accounts.google.com/o/oauth2/auth", "token_uri": "https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/sa-id%40project.iam.gserviceaccount.com", "universe_domain": "googleapis.com" } ` var ( provider = "my-provider" bannerID = "my-banner-id" storeID = "my-store-id" terminalID = "my-terminal-id" testHostname = "test-ienode" ) func defaultSecret() corev1.Secret { secret := corev1.Secret{ Data: map[string][]byte{ "config.yaml.tpl": []byte(tplConfig), "key.json": []byte(adcKey), }, } return secret } type mockConf struct { fakeIENode v1ien.IENode err error config.Config } func (mConf mockConf) GetHostIENode(_ context.Context) (*v1ien.IENode, error) { return &mConf.fakeIENode, mConf.err } func TestMain(m *testing.M) { f = f2.New(context.Background(), f2.WithExtensions()). Setup(). Teardown() os.Exit(f.Run(m)) } func TestGetTerminalID(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("gets terminal ID", func(ctx f2.Context, t *testing.T) f2.Context { fakeIENode := &v1ien.IENode{ ObjectMeta: metav1.ObjectMeta{ Name: testHostname, Labels: map[string]string{"node.ncr.com/terminal-id": terminalID}, }, } mConf := mockConf{ fakeIENode: *fakeIENode, err: nil, } expTerminalInfo := map[string]string{"terminalID": terminalID} gotTerminalInfo, err := getTerminalInfo(ctx, mConf) assert.NoError(t, err) assert.Equal(t, expTerminalInfo, gotTerminalInfo) return ctx }).Feature() f.Test(t, feature) } func TestGetTerminalIDError(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("gets terminal ID error", func(ctx f2.Context, t *testing.T) f2.Context { expErr := fmt.Errorf("GetHostIENode Error") fakeIENode := &v1ien.IENode{ ObjectMeta: metav1.ObjectMeta{ Name: testHostname, Labels: map[string]string{"node.ncr.com/terminal-id": terminalID}, }, } mConf := mockConf{ fakeIENode: *fakeIENode, err: expErr, } gotTerminalInfo, err := getTerminalInfo(ctx, mConf) assert.ErrorIs(t, err, expErr) assert.ErrorContains(t, err, "locating IENode") assert.Empty(t, gotTerminalInfo) return ctx }).Feature() f.Test(t, feature) } func TestIsTargetConfigMap(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("is target config map", func(ctx f2.Context, t *testing.T) f2.Context { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "remote-agent-configuration", Namespace: "sds", }, } assert.True(t, isTargetSecret(&secret)) return ctx }).Feature() f.Test(t, feature) } func TestWrongName(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("wrong name", func(ctx f2.Context, t *testing.T) f2.Context { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "node-agent-plugins", Namespace: "sds", }, } assert.False(t, isTargetSecret(&secret)) return ctx }).Feature() f.Test(t, feature) } func TestWrongNamespace(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("wrong namespace", func(ctx f2.Context, t *testing.T) f2.Context { secret := corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "remote-agent-configuration", Namespace: "kube-public", }, } assert.False(t, isTargetSecret(&secret)) return ctx }).Feature() f.Test(t, feature) } // Return mock.MatchedBy func to verify the adc filename follows the expected // pattern and store the given filename in the name param // TODO could verify timestamp is reasonable func adcFileNameChecker(storedName *string) func(filename string) bool { return func(filename string) bool { // This check may be called multiple times with different data as the // SafeWrite method may be called twice and mock seems to run the check // for evey call. We only want to check the filename when writing the // adc.json, not when writing the config.yaml if strings.HasSuffix(filename, ".adc.json") { *storedName = filename return true } return false } } // Verify the contents of the config file are as expected func configFileChecker(adcFilename *string) func(contents []byte) bool { return func(contents []byte) bool { // This check function may be called multiple times, e.g. when writing the // adc and when writing the config.yaml. As this check is only for the // config.yaml we should return false when writing the adc if string(contents) == adcKey { return false } return string(contents) == fmt.Sprintf(expConfig, "/data/remote-access-agent", *adcFilename) } } func TestUpdateRemoteAgentConfig(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("updates remoteagentconfig", func(ctx f2.Context, t *testing.T) f2.Context { secret := defaultSecret() terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } // Filename that the adc key has been saved to, need to know it to verify // the expected config also uses the correct key var adcFilename = "PLACEHOLDER" fakeFileHandler, fileMock := fakeFile.NewFake() fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("", fs.ErrNotExist).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(&adcFilename)), fs.FileMode(0644)).Return(nil).Once() err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.NoError(t, err) fileMock.AssertExpectations(t) assert.NotEqual(t, adcFilename, "PLACEHOLDER") return ctx }).Feature() f.Test(t, feature) } func TestUpdatesOldFile(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("updates old file", func(ctx f2.Context, t *testing.T) f2.Context { secret := defaultSecret() terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } var adcFilename = "PLACEHOLDER" fakeFileHandler, fileMock := fakeFile.NewFake() fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("provider: \"None\"", nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(&adcFilename)), fs.FileMode(0644)).Return(nil).Once() err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.NoError(t, err) fileMock.AssertExpectations(t) assert.NotEqual(t, adcFilename, "PLACEHOLDER") return ctx }).Feature() f.Test(t, feature) } func TestUpdateRemoteAgentConfigError(t *testing.T) { t.Parallel() tests := map[string]struct { mock func(*string, *mock.Mock) }{ "Error writing adc key": { mock: func(adcFilename *string, fileMock *mock.Mock) { fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(fmt.Errorf("error writing adc")).Once() }, }, "Error writing config.yaml": { mock: func(adcFilename *string, fileMock *mock.Mock) { fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("provider: \"None\"", nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(adcFilename)), fs.FileMode(0644)).Return(fmt.Errorf("error writing config")).Once() }, }, } for name, tc := range tests { tc := tc t.Run(name, func(t *testing.T) { t.Parallel() secret := defaultSecret() terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } var adcFilename = "PLACEHOLDER" fakeFileHandler, fileMock := fakeFile.NewFake() tc.mock(&adcFilename, fileMock) err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.Error(t, err) fileMock.AssertExpectations(t) assert.NotEqual(t, adcFilename, "PLACEHOLDER") }) } } func TestDoesntUpdateExistingConfig(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("doesnt update existing config", func(ctx f2.Context, t *testing.T) f2.Context { secret := defaultSecret() terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } adcFilename := "16.adc.json" fakeFileHandler, fileMock := fakeFile.NewFake() fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{dirEntryMock{name: adcFilename}}, nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+adcFilename).Return(adcKey, nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return(fmt.Sprintf(expConfig, "/data/remote-access-agent", adcFilename), nil).Once() err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.NoError(t, err) fileMock.AssertExpectations(t) return ctx }).Feature() f.Test(t, feature) } func TestMissingConfigTemplate(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("missing config template", func(ctx f2.Context, t *testing.T) f2.Context { secret := corev1.Secret{ Data: map[string][]byte{ "config.yaml": []byte(tplConfig), // Real key is config.yaml.tpl "key.json": []byte(adcKey), }, } terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } var adcFilename = "PLACEHOLDER" fakeFileHandler, fileMock := fakeFile.NewFake() fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once() fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once() err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.ErrorContains(t, err, "cannot find template config.yaml.tpl in secret") fileMock.AssertExpectations(t) assert.NotEqual(t, adcFilename, "PLACEHOLDER") return ctx }).Feature() f.Test(t, feature) } func TestMissingADCKey(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("missing ADC key", func(ctx f2.Context, t *testing.T) f2.Context { secret := corev1.Secret{ Data: map[string][]byte{ "config.yaml.tpl": []byte(tplConfig), // Real key is config.yaml.tpl }, } terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } fakeFileHandler, fileMock := fakeFile.NewFake() // No expected file calls err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.ErrorContains(t, err, "cannot find adc entry key.json in secret") fileMock.AssertExpectations(t) return ctx }).Feature() f.Test(t, feature) } func TestADCCleanup(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("ADC cleanup", func(ctx f2.Context, t *testing.T) f2.Context { secret := defaultSecret() terminalInfo := map[string]string{"terminalID": terminalID} fakeEdgeInfo := info.EdgeInfo{ BannerEdgeID: bannerID, ClusterEdgeID: storeID, ProjectID: provider, } var adcFilename = "1687187279.adc.json" var oldFilename = "1687187219.adc.json" // 60 seconds earlier; fakeFileHandler, fileMock := fakeFile.NewFake() files := []fs.DirEntry{ dirEntryMock{name: "my.adc.json"}, dirEntryMock{name: "my.1687187279.adc.json"}, dirEntryMock{name: adcFilename}, dirEntryMock{name: "config.yaml"}, dirEntryMock{name: "config.yaml.bk"}, dirEntryMock{name: oldFilename}, } fileMock.On("ReadDir", remoteAgentHostDataDir).Return(files, nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+adcFilename).Return(adcKey, nil).Once() fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return(fmt.Sprintf(expConfig, "/data/remote-access-agent", adcFilename), nil).Once() // Does not write either adc or config.yaml fileMock.On("Remove", remoteAgentHostDataDir+"/"+oldFilename).Return(nil, nil).Once() err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler) assert.NoError(t, err) fileMock.AssertExpectations(t) return ctx }).Feature() f.Test(t, feature) } type dirEntryMock struct { name string fs.DirEntry } func (de dirEntryMock) Name() string { return de.name } func TestSortedADCFiles(t *testing.T) { feature := f2.NewFeature("Remoteagentconfig plugin"). Test("sorted ADC files", func(ctx f2.Context, t *testing.T) f2.Context { fakeFileHandler, fileMock := fakeFile.NewFake() fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{dirEntryMock{name: "my.adc.json"}, dirEntryMock{name: "my.1687187279.adc.json"}, dirEntryMock{name: "1687187279.adc.json"}, dirEntryMock{name: remoteAgentConfigFileName}, dirEntryMock{name: "1687187219.adc.json"}}, nil).Once() res, err := sortedADCFiles(fakeFileHandler) assert.NoError(t, err) assert.Equal(t, []string{"1687187219.adc.json", "1687187279.adc.json"}, res) return ctx }).Feature() f.Test(t, feature) }