...

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

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

     1  package remoteagentconfig
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io/fs"
     7  	"os"
     8  	"strings"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/mock"
    13  	corev1 "k8s.io/api/core/v1"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  
    16  	"edge-infra.dev/pkg/edge/info"
    17  	v1ien "edge-infra.dev/pkg/sds/ien/k8s/apis/v1"
    18  	"edge-infra.dev/pkg/sds/ien/k8s/controllers/nodeagent/config"
    19  	fakeFile "edge-infra.dev/pkg/sds/lib/os/file/fake"
    20  	"edge-infra.dev/test/f2"
    21  )
    22  
    23  var f f2.Framework
    24  
    25  var tplConfig = `
    26  provider: "{{ .Provider }}"
    27  
    28  subscriptions:
    29    - name: "sub_{{ .BannerID }}_{{ .StoreID }}_{{ .TerminalID }}_dsds-ea-request
    30      bannerID: "{{ .BannerID }}"
    31      storeID: "{{ .StoreID }}"
    32      terminalID: "{{ .TerminalID }}"
    33      CredentialsPath: "{{ .CredentialsPath }}"
    34      handler:
    35        ResponseTopic: "topic_{{ .BannerID }}_dsds-ea-request"
    36        Type: "cli"
    37  `
    38  
    39  var expConfig = `
    40  provider: "my-provider"
    41  
    42  subscriptions:
    43    - name: "sub_my-banner-id_my-store-id_my-terminal-id_dsds-ea-request
    44      bannerID: "my-banner-id"
    45      storeID: "my-store-id"
    46      terminalID: "my-terminal-id"
    47      CredentialsPath: "%s/%s"
    48      handler:
    49        ResponseTopic: "topic_my-banner-id_dsds-ea-request"
    50        Type: "cli"
    51  `
    52  
    53  //nolint:gosec // Secret template, not secret
    54  var adcKey = `
    55  {
    56    "type": "service_account",
    57    "project_id": "project",
    58    "private_key_id": "id",
    59    "private_key": "-----BEGIN PRIVATE KEY-----\nKey Data\n-----END PRIVATE KEY-----\n",
    60    "client_email": "sa-id@project.iam.gserviceaccount.com",
    61    "client_id": "id",
    62    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    63    "token_uri": "https://oauth2.googleapis.com/token",
    64    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    65    "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/sa-id%40project.iam.gserviceaccount.com",
    66    "universe_domain": "googleapis.com"
    67  }
    68  `
    69  
    70  var (
    71  	provider   = "my-provider"
    72  	bannerID   = "my-banner-id"
    73  	storeID    = "my-store-id"
    74  	terminalID = "my-terminal-id"
    75  
    76  	testHostname = "test-ienode"
    77  )
    78  
    79  func defaultSecret() corev1.Secret {
    80  	secret := corev1.Secret{
    81  		Data: map[string][]byte{
    82  			"config.yaml.tpl": []byte(tplConfig),
    83  			"key.json":        []byte(adcKey),
    84  		},
    85  	}
    86  	return secret
    87  }
    88  
    89  type mockConf struct {
    90  	fakeIENode v1ien.IENode
    91  	err        error
    92  	config.Config
    93  }
    94  
    95  func (mConf mockConf) GetHostIENode(_ context.Context) (*v1ien.IENode, error) {
    96  	return &mConf.fakeIENode, mConf.err
    97  }
    98  
    99  func TestMain(m *testing.M) {
   100  	f = f2.New(context.Background(), f2.WithExtensions()).
   101  		Setup().
   102  		Teardown()
   103  	os.Exit(f.Run(m))
   104  }
   105  
   106  func TestGetTerminalID(t *testing.T) {
   107  	feature := f2.NewFeature("Remoteagentconfig plugin").
   108  		Test("gets terminal ID", func(ctx f2.Context, t *testing.T) f2.Context {
   109  			fakeIENode := &v1ien.IENode{
   110  				ObjectMeta: metav1.ObjectMeta{
   111  					Name:   testHostname,
   112  					Labels: map[string]string{"node.ncr.com/terminal-id": terminalID},
   113  				},
   114  			}
   115  			mConf := mockConf{
   116  				fakeIENode: *fakeIENode,
   117  				err:        nil,
   118  			}
   119  			expTerminalInfo := map[string]string{"terminalID": terminalID}
   120  
   121  			gotTerminalInfo, err := getTerminalInfo(ctx, mConf)
   122  
   123  			assert.NoError(t, err)
   124  			assert.Equal(t, expTerminalInfo, gotTerminalInfo)
   125  
   126  			return ctx
   127  		}).Feature()
   128  
   129  	f.Test(t, feature)
   130  }
   131  
   132  func TestGetTerminalIDError(t *testing.T) {
   133  	feature := f2.NewFeature("Remoteagentconfig plugin").
   134  		Test("gets terminal ID error", func(ctx f2.Context, t *testing.T) f2.Context {
   135  			expErr := fmt.Errorf("GetHostIENode Error")
   136  			fakeIENode := &v1ien.IENode{
   137  				ObjectMeta: metav1.ObjectMeta{
   138  					Name:   testHostname,
   139  					Labels: map[string]string{"node.ncr.com/terminal-id": terminalID},
   140  				},
   141  			}
   142  			mConf := mockConf{
   143  				fakeIENode: *fakeIENode,
   144  				err:        expErr,
   145  			}
   146  
   147  			gotTerminalInfo, err := getTerminalInfo(ctx, mConf)
   148  
   149  			assert.ErrorIs(t, err, expErr)
   150  			assert.ErrorContains(t, err, "locating IENode")
   151  			assert.Empty(t, gotTerminalInfo)
   152  
   153  			return ctx
   154  		}).Feature()
   155  
   156  	f.Test(t, feature)
   157  }
   158  
   159  func TestIsTargetConfigMap(t *testing.T) {
   160  	feature := f2.NewFeature("Remoteagentconfig plugin").
   161  		Test("is target config map", func(ctx f2.Context, t *testing.T) f2.Context {
   162  			secret := corev1.Secret{
   163  				ObjectMeta: metav1.ObjectMeta{
   164  					Name:      "remote-agent-configuration",
   165  					Namespace: "sds",
   166  				},
   167  			}
   168  
   169  			assert.True(t, isTargetSecret(&secret))
   170  
   171  			return ctx
   172  		}).Feature()
   173  
   174  	f.Test(t, feature)
   175  }
   176  
   177  func TestWrongName(t *testing.T) {
   178  	feature := f2.NewFeature("Remoteagentconfig plugin").
   179  		Test("wrong name", func(ctx f2.Context, t *testing.T) f2.Context {
   180  			secret := corev1.Secret{
   181  				ObjectMeta: metav1.ObjectMeta{
   182  					Name:      "node-agent-plugins",
   183  					Namespace: "sds",
   184  				},
   185  			}
   186  
   187  			assert.False(t, isTargetSecret(&secret))
   188  
   189  			return ctx
   190  		}).Feature()
   191  
   192  	f.Test(t, feature)
   193  }
   194  
   195  func TestWrongNamespace(t *testing.T) {
   196  	feature := f2.NewFeature("Remoteagentconfig plugin").
   197  		Test("wrong namespace", func(ctx f2.Context, t *testing.T) f2.Context {
   198  			secret := corev1.Secret{
   199  				ObjectMeta: metav1.ObjectMeta{
   200  					Name:      "remote-agent-configuration",
   201  					Namespace: "kube-public",
   202  				},
   203  			}
   204  
   205  			assert.False(t, isTargetSecret(&secret))
   206  
   207  			return ctx
   208  		}).Feature()
   209  
   210  	f.Test(t, feature)
   211  }
   212  
   213  // Return mock.MatchedBy func to verify the adc filename follows the expected
   214  // pattern and store the given filename in the name param
   215  // TODO could verify timestamp is reasonable
   216  func adcFileNameChecker(storedName *string) func(filename string) bool {
   217  	return func(filename string) bool {
   218  		// This check may be called multiple times with different data as the
   219  		// SafeWrite method may be called twice and mock seems to run the check
   220  		// for evey call. We only want to check the filename when writing the
   221  		// adc.json, not when writing the config.yaml
   222  		if strings.HasSuffix(filename, ".adc.json") {
   223  			*storedName = filename
   224  			return true
   225  		}
   226  		return false
   227  	}
   228  }
   229  
   230  // Verify the contents of the config file are as expected
   231  func configFileChecker(adcFilename *string) func(contents []byte) bool {
   232  	return func(contents []byte) bool {
   233  		// This check function may be called multiple times, e.g. when writing the
   234  		// adc and when writing the config.yaml. As this check is only for the
   235  		// config.yaml we should return false when writing the adc
   236  		if string(contents) == adcKey {
   237  			return false
   238  		}
   239  		return string(contents) == fmt.Sprintf(expConfig, "/data/remote-access-agent", *adcFilename)
   240  	}
   241  }
   242  
   243  func TestUpdateRemoteAgentConfig(t *testing.T) {
   244  	feature := f2.NewFeature("Remoteagentconfig plugin").
   245  		Test("updates remoteagentconfig", func(ctx f2.Context, t *testing.T) f2.Context {
   246  			secret := defaultSecret()
   247  			terminalInfo := map[string]string{"terminalID": terminalID}
   248  			fakeEdgeInfo := info.EdgeInfo{
   249  				BannerEdgeID:  bannerID,
   250  				ClusterEdgeID: storeID,
   251  				ProjectID:     provider,
   252  			}
   253  
   254  			// Filename that the adc key has been saved to, need to know it to verify
   255  			// the expected config also uses the correct key
   256  			var adcFilename = "PLACEHOLDER"
   257  
   258  			fakeFileHandler, fileMock := fakeFile.NewFake()
   259  
   260  			fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once()
   261  			fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once()
   262  			fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("", fs.ErrNotExist).Once()
   263  
   264  			fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(&adcFilename)), fs.FileMode(0644)).Return(nil).Once()
   265  
   266  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   267  
   268  			assert.NoError(t, err)
   269  
   270  			fileMock.AssertExpectations(t)
   271  
   272  			assert.NotEqual(t, adcFilename, "PLACEHOLDER")
   273  
   274  			return ctx
   275  		}).Feature()
   276  
   277  	f.Test(t, feature)
   278  }
   279  
   280  func TestUpdatesOldFile(t *testing.T) {
   281  	feature := f2.NewFeature("Remoteagentconfig plugin").
   282  		Test("updates old file", func(ctx f2.Context, t *testing.T) f2.Context {
   283  			secret := defaultSecret()
   284  			terminalInfo := map[string]string{"terminalID": terminalID}
   285  			fakeEdgeInfo := info.EdgeInfo{
   286  				BannerEdgeID:  bannerID,
   287  				ClusterEdgeID: storeID,
   288  				ProjectID:     provider,
   289  			}
   290  
   291  			var adcFilename = "PLACEHOLDER"
   292  
   293  			fakeFileHandler, fileMock := fakeFile.NewFake()
   294  			fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once()
   295  			fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once()
   296  
   297  			fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("provider: \"None\"", nil).Once()
   298  
   299  			fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(&adcFilename)), fs.FileMode(0644)).Return(nil).Once()
   300  
   301  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   302  
   303  			assert.NoError(t, err)
   304  
   305  			fileMock.AssertExpectations(t)
   306  
   307  			assert.NotEqual(t, adcFilename, "PLACEHOLDER")
   308  
   309  			return ctx
   310  		}).Feature()
   311  
   312  	f.Test(t, feature)
   313  }
   314  
   315  func TestUpdateRemoteAgentConfigError(t *testing.T) {
   316  	t.Parallel()
   317  
   318  	tests := map[string]struct {
   319  		mock func(*string, *mock.Mock)
   320  	}{
   321  		"Error writing adc key": {
   322  			mock: func(adcFilename *string, fileMock *mock.Mock) {
   323  				fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once()
   324  				fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(fmt.Errorf("error writing adc")).Once()
   325  			},
   326  		},
   327  		"Error writing config.yaml": {
   328  			mock: func(adcFilename *string, fileMock *mock.Mock) {
   329  				fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once()
   330  				fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once()
   331  
   332  				fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return("provider: \"None\"", nil).Once()
   333  
   334  				fileMock.On("SafeWrite", remoteAgentHostDataDir, "config.yaml", mock.MatchedBy(configFileChecker(adcFilename)), fs.FileMode(0644)).Return(fmt.Errorf("error writing config")).Once()
   335  			},
   336  		},
   337  	}
   338  
   339  	for name, tc := range tests {
   340  		tc := tc
   341  		t.Run(name, func(t *testing.T) {
   342  			t.Parallel()
   343  
   344  			secret := defaultSecret()
   345  			terminalInfo := map[string]string{"terminalID": terminalID}
   346  			fakeEdgeInfo := info.EdgeInfo{
   347  				BannerEdgeID:  bannerID,
   348  				ClusterEdgeID: storeID,
   349  				ProjectID:     provider,
   350  			}
   351  
   352  			var adcFilename = "PLACEHOLDER"
   353  
   354  			fakeFileHandler, fileMock := fakeFile.NewFake()
   355  
   356  			tc.mock(&adcFilename, fileMock)
   357  
   358  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   359  			assert.Error(t, err)
   360  
   361  			fileMock.AssertExpectations(t)
   362  
   363  			assert.NotEqual(t, adcFilename, "PLACEHOLDER")
   364  		})
   365  	}
   366  }
   367  
   368  func TestDoesntUpdateExistingConfig(t *testing.T) {
   369  	feature := f2.NewFeature("Remoteagentconfig plugin").
   370  		Test("doesnt update existing config", func(ctx f2.Context, t *testing.T) f2.Context {
   371  			secret := defaultSecret()
   372  			terminalInfo := map[string]string{"terminalID": terminalID}
   373  			fakeEdgeInfo := info.EdgeInfo{
   374  				BannerEdgeID:  bannerID,
   375  				ClusterEdgeID: storeID,
   376  				ProjectID:     provider,
   377  			}
   378  
   379  			adcFilename := "16.adc.json"
   380  
   381  			fakeFileHandler, fileMock := fakeFile.NewFake()
   382  			fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{dirEntryMock{name: adcFilename}}, nil).Once()
   383  			fileMock.On("Read", remoteAgentHostDataDir+"/"+adcFilename).Return(adcKey, nil).Once()
   384  
   385  			fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return(fmt.Sprintf(expConfig, "/data/remote-access-agent", adcFilename), nil).Once()
   386  
   387  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   388  
   389  			assert.NoError(t, err)
   390  
   391  			fileMock.AssertExpectations(t)
   392  
   393  			return ctx
   394  		}).Feature()
   395  
   396  	f.Test(t, feature)
   397  }
   398  
   399  func TestMissingConfigTemplate(t *testing.T) {
   400  	feature := f2.NewFeature("Remoteagentconfig plugin").
   401  		Test("missing config template", func(ctx f2.Context, t *testing.T) f2.Context {
   402  			secret := corev1.Secret{
   403  				Data: map[string][]byte{
   404  					"config.yaml": []byte(tplConfig), // Real key is config.yaml.tpl
   405  					"key.json":    []byte(adcKey),
   406  				},
   407  			}
   408  			terminalInfo := map[string]string{"terminalID": terminalID}
   409  			fakeEdgeInfo := info.EdgeInfo{
   410  				BannerEdgeID:  bannerID,
   411  				ClusterEdgeID: storeID,
   412  				ProjectID:     provider,
   413  			}
   414  
   415  			var adcFilename = "PLACEHOLDER"
   416  
   417  			fakeFileHandler, fileMock := fakeFile.NewFake()
   418  			fileMock.On("ReadDir", remoteAgentHostDataDir).Return([]fs.DirEntry{}, nil).Once()
   419  			fileMock.On("SafeWrite", remoteAgentHostDataDir, mock.MatchedBy(adcFileNameChecker(&adcFilename)), []byte(adcKey), fs.FileMode(0644)).Return(nil).Once()
   420  
   421  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   422  
   423  			assert.ErrorContains(t, err, "cannot find template config.yaml.tpl in secret")
   424  			fileMock.AssertExpectations(t)
   425  
   426  			assert.NotEqual(t, adcFilename, "PLACEHOLDER")
   427  
   428  			return ctx
   429  		}).Feature()
   430  
   431  	f.Test(t, feature)
   432  }
   433  
   434  func TestMissingADCKey(t *testing.T) {
   435  	feature := f2.NewFeature("Remoteagentconfig plugin").
   436  		Test("missing ADC key", func(ctx f2.Context, t *testing.T) f2.Context {
   437  			secret := corev1.Secret{
   438  				Data: map[string][]byte{
   439  					"config.yaml.tpl": []byte(tplConfig), // Real key is config.yaml.tpl
   440  				},
   441  			}
   442  			terminalInfo := map[string]string{"terminalID": terminalID}
   443  			fakeEdgeInfo := info.EdgeInfo{
   444  				BannerEdgeID:  bannerID,
   445  				ClusterEdgeID: storeID,
   446  				ProjectID:     provider,
   447  			}
   448  
   449  			fakeFileHandler, fileMock := fakeFile.NewFake()
   450  			// No expected file calls
   451  
   452  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   453  
   454  			assert.ErrorContains(t, err, "cannot find adc entry key.json in secret")
   455  			fileMock.AssertExpectations(t)
   456  
   457  			return ctx
   458  		}).Feature()
   459  
   460  	f.Test(t, feature)
   461  }
   462  
   463  func TestADCCleanup(t *testing.T) {
   464  	feature := f2.NewFeature("Remoteagentconfig plugin").
   465  		Test("ADC cleanup", func(ctx f2.Context, t *testing.T) f2.Context {
   466  			secret := defaultSecret()
   467  			terminalInfo := map[string]string{"terminalID": terminalID}
   468  			fakeEdgeInfo := info.EdgeInfo{
   469  				BannerEdgeID:  bannerID,
   470  				ClusterEdgeID: storeID,
   471  				ProjectID:     provider,
   472  			}
   473  
   474  			var adcFilename = "1687187279.adc.json"
   475  			var oldFilename = "1687187219.adc.json" // 60 seconds earlier;
   476  
   477  			fakeFileHandler, fileMock := fakeFile.NewFake()
   478  
   479  			files := []fs.DirEntry{
   480  				dirEntryMock{name: "my.adc.json"},
   481  				dirEntryMock{name: "my.1687187279.adc.json"},
   482  				dirEntryMock{name: adcFilename},
   483  				dirEntryMock{name: "config.yaml"},
   484  				dirEntryMock{name: "config.yaml.bk"},
   485  				dirEntryMock{name: oldFilename},
   486  			}
   487  
   488  			fileMock.On("ReadDir", remoteAgentHostDataDir).Return(files, nil).Once()
   489  			fileMock.On("Read", remoteAgentHostDataDir+"/"+adcFilename).Return(adcKey, nil).Once()
   490  			fileMock.On("Read", remoteAgentHostDataDir+"/"+remoteAgentConfigFileName).Return(fmt.Sprintf(expConfig, "/data/remote-access-agent", adcFilename), nil).Once()
   491  
   492  			// Does not write either adc or config.yaml
   493  
   494  			fileMock.On("Remove", remoteAgentHostDataDir+"/"+oldFilename).Return(nil, nil).Once()
   495  
   496  			err := UpdateRemoteAgentConfig(context.Background(), &secret, terminalInfo, &fakeEdgeInfo, fakeFileHandler)
   497  
   498  			assert.NoError(t, err)
   499  
   500  			fileMock.AssertExpectations(t)
   501  
   502  			return ctx
   503  		}).Feature()
   504  
   505  	f.Test(t, feature)
   506  }
   507  
   508  type dirEntryMock struct {
   509  	name string
   510  	fs.DirEntry
   511  }
   512  
   513  func (de dirEntryMock) Name() string {
   514  	return de.name
   515  }
   516  
   517  func TestSortedADCFiles(t *testing.T) {
   518  	feature := f2.NewFeature("Remoteagentconfig plugin").
   519  		Test("sorted ADC files", func(ctx f2.Context, t *testing.T) f2.Context {
   520  			fakeFileHandler, fileMock := fakeFile.NewFake()
   521  			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()
   522  
   523  			res, err := sortedADCFiles(fakeFileHandler)
   524  			assert.NoError(t, err)
   525  
   526  			assert.Equal(t, []string{"1687187219.adc.json", "1687187279.adc.json"}, res)
   527  
   528  			return ctx
   529  		}).Feature()
   530  
   531  	f.Test(t, feature)
   532  }
   533  

View as plain text