// Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may not // use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0/ // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or // implied. See the License for the specific language governing // permissions and limitations under the License. // Package ssminstaller implements the installer for ssm packages that use documents or scripts to install and uninstall. package ssminstaller import ( "io/ioutil" "path/filepath" "testing" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/contracts" "github.com/aws/amazon-ssm-agent/agent/fileutil" contextmocks "github.com/aws/amazon-ssm-agent/agent/mocks/context" "github.com/aws/amazon-ssm-agent/agent/mocks/log" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect/ec2infradetect" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect/osdetect" envdetectmocks "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/mocks/envdetect" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) const testPackagePath = "testdata" var contextMock context.T = contextmocks.NewMockDefault() func TestPackageName(t *testing.T) { testName := "TestName" testVersion := "TestVersion" inst := Installer{packageName: testName, version: testVersion} assert.Equal(t, testName, inst.packageName) } func TestPackageVersion(t *testing.T) { testName := "TestName" testVersion := "TestVersion" inst := Installer{packageName: testName, version: testVersion} assert.Equal(t, testVersion, inst.version) } func mockReadAction(t *testing.T, mockFileSys *MockedFileSys, actionPathNoExt string, contentSh []byte, contentPs1 []byte, expectReads bool) { // Setup mock with expectations mockFileSys.On("Exists", actionPathNoExt+".sh").Return(len(contentSh) != 0).Once() mockFileSys.On("Exists", actionPathNoExt+".ps1").Return(len(contentPs1) != 0).Once() if expectReads { if len(contentSh) != 0 { mockFileSys.On("ReadFile", actionPathNoExt+".sh").Return(contentSh, nil).Once() } if len(contentPs1) != 0 { mockFileSys.On("ReadFile", actionPathNoExt+".ps1").Return(contentPs1, nil).Once() } } } var environmentStub = envdetect.Environment{ &osdetect.OperatingSystem{"abc", "567", "", "xyz", "", ""}, &ec2infradetect.Ec2Infrastructure{"instanceIDX", "Reg1", "", "AZ1", "instanceTypeZ"}, } func testReadAction(t *testing.T, actionPathNoExt string, contentSh []byte, contentPs1 []byte, expectReads bool) { mockFileSys := MockedFileSys{} mockReadAction(t, &mockFileSys, actionPathNoExt, contentSh, contentPs1, expectReads) mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) tracer.BeginSection("test segment root") // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value exists, actionDoc, workingDir, orchestrationDir, err := inst.readAction(tracer, contextMock, "Foo") mockFileSys.AssertExpectations(t) assert.True(t, exists) assert.NotEmpty(t, actionDoc) assert.Equal(t, workingDir, testPackagePath) assert.Equal(t, orchestrationDir, "Foo") assert.Nil(t, err) } func testReadActionInvalid(t *testing.T, actionPathNoExt string, contentSh []byte, contentPs1 []byte, expectReads bool) { mockFileSys := MockedFileSys{} mockReadAction(t, &mockFileSys, actionPathNoExt, contentSh, contentPs1, expectReads) mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) tracer.BeginSection("test segment root") // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value exists, actionDoc, workingDir, orchestrationDir, err := inst.readAction(tracer, contextMock, "Foo") mockFileSys.AssertExpectations(t) assert.True(t, exists) assert.Empty(t, actionDoc) assert.Empty(t, workingDir) assert.Empty(t, orchestrationDir) assert.NotNil(t, err) } func TestReadAction(t *testing.T) { actionPathNoExt := filepath.Join(testPackagePath, "Foo") testReadAction(t, actionPathNoExt, []byte("echo sh"), []byte{}, false) testReadAction(t, actionPathNoExt, []byte{}, append(fileutil.CreateUTF8ByteOrderMark(), []byte("Write-Host ps1 with BOM")...), false) } func TestReadActionMissing(t *testing.T) { mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "Foo") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte{}, []byte{}, false) mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) tracer.BeginSection("test segment root") // Instantiate repository with mock repo := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value exists, actionDoc, workingDir, orchestrationDir, err := repo.readAction(tracer, contextMock, "Foo") mockFileSys.AssertExpectations(t) assert.False(t, exists) assert.Empty(t, actionDoc) assert.Empty(t, workingDir) assert.Empty(t, orchestrationDir) assert.Nil(t, err) } func testReadActionTooManyActionImplementations(t *testing.T, existSh bool, existPs1 bool) { mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "Foo") mockFileSys.On("Exists", actionPathNoExt+".sh").Return(existSh).Once() mockFileSys.On("Exists", actionPathNoExt+".ps1").Return(existPs1).Once() mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) tracer.BeginSection("test segment root") // Instantiate repository with mock repo := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value exists, actionDoc, workingDir, orchestrationDir, err := repo.readAction(tracer, contextMock, "Foo") mockFileSys.AssertExpectations(t) assert.True(t, exists) assert.Empty(t, actionDoc) assert.Empty(t, workingDir) assert.Empty(t, orchestrationDir) assert.NotNil(t, err) } func TestReadActionTooManyActionImplementations(t *testing.T) { testReadActionTooManyActionImplementations(t, true, true) } var envVars = map[string]string{ "key1": "val1", "key2": "val2", } func TestReadShActionWithEnvVars(t *testing.T) { mockFileSys := MockedFileSys{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} action := &Action{} action.actionName = "install" action.actionType = ACTION_TYPE_SH pluginsInfo, err := inst.readShAction(contextMock, action, "Foo", "", envVars) // Call and validate mock expectations and return value assert.Nil(t, err) assert.NotEmpty(t, pluginsInfo) assert.Equal(t, 1, len(pluginsInfo)) pluginInput := pluginsInfo[0].Configuration.Properties assert.NotEmpty(t, pluginInput) pluginInputMap, _ := pluginInput.(map[string]interface{}) assert.Equal(t, envVars, pluginInputMap["environment"]) } func TestReadPs1ActionWithEnvVars(t *testing.T) { mockFileSys := MockedFileSys{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} action := &Action{} action.actionName = "uninstall" action.actionType = ACTION_TYPE_PS1 pluginsInfo, err := inst.readPs1Action(contextMock, action, "Foo", "", envVars) // Call and validate mock expectations and return value assert.Nil(t, err) assert.NotEmpty(t, pluginsInfo) assert.Equal(t, 1, len(pluginsInfo)) pluginInput := pluginsInfo[0].Configuration.Properties assert.NotEmpty(t, pluginInput) pluginInputMap, _ := pluginInput.(map[string]interface{}) assert.Equal(t, envVars, pluginInputMap["environment"]) } func TestGetEnvVarsContainsAdditionalArguments(t *testing.T) { mockFileSys := MockedFileSys{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() var argumentString = "{\"customArg1\":\"customVal1\", \"customArg2\":\"customVal2\"}" // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector, additionalArguments: argumentString} envVars, err := inst.getEnvVars("install", contextMock) // Call and validate mock expectations and return value assert.Nil(t, err) assert.NotEmpty(t, envVars) assert.Contains(t, envVars, "customArg1") assert.Equal(t, "customVal1", envVars["customArg1"]) assert.Contains(t, envVars, "customArg2") assert.Equal(t, "customVal2", envVars["customArg2"]) } func TestGetEnvVarsWithEmptyAdditionalArguments(t *testing.T) { mockFileSys := MockedFileSys{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() var argumentString = "" // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector, additionalArguments: argumentString} envVars, err := inst.getEnvVars("install", contextMock) // Call and validate mock expectations and return value assert.Nil(t, err) assert.NotEmpty(t, envVars) assert.NotContains(t, envVars, "customArg1") assert.NotContains(t, envVars, "customArg2") } func TestInstall_ExecuteError(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "install") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte("echo sh"), []byte{}, false) mockExec := MockedExec{} mockExec.On("ExecuteDocument", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string]*contracts.PluginResult{"Foo": {StandardError: "execute error"}}).Once() mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Install(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.NotEmpty(t, output.GetStderr()) assert.Contains(t, output.GetStderr(), "execute error") } func TestValidate_NoAction(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "validate") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte{}, []byte{}, false) mockExec := MockedExec{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Validate(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.Empty(t, output.GetStderr()) assert.Equal(t, 0, output.GetExitCode()) assert.Equal(t, contracts.ResultStatusSuccess, output.GetStatus()) } func TestUninstall_Success(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "uninstall") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte("echo sh"), []byte{}, false) mockExec := MockedExec{} mockExec.On("ExecuteDocument", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string]*contracts.PluginResult{"Foo": {Status: contracts.ResultStatusSuccess}}).Once() mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, config: contracts.Configuration{OutputS3BucketName: "foo", OutputS3KeyPrefix: "bar"}, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Uninstall(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.Empty(t, output.GetStderr()) assert.Equal(t, 0, output.GetExitCode()) assert.Equal(t, contracts.ResultStatusSuccess, output.GetStatus()) } func TestUpdate_Success(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "update") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte("echo sh"), []byte{}, false) mockExec := MockedExec{} mockExec.On("ExecuteDocument", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string]*contracts.PluginResult{"Foo": {Status: contracts.ResultStatusSuccess}}).Once() mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, config: contracts.Configuration{OutputS3BucketName: "foo", OutputS3KeyPrefix: "bar"}, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Update(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.Empty(t, output.GetStderr()) assert.Equal(t, 0, output.GetExitCode()) assert.Equal(t, contracts.ResultStatusSuccess, output.GetStatus()) } func TestUpdate_ExecuteError(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "update") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte("echo sh"), []byte{}, false) mockExec := MockedExec{} mockExec.On("ExecuteDocument", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(map[string]*contracts.PluginResult{"Foo": {StandardError: "execute error"}}).Once() mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Update(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.NotEmpty(t, output.GetStderr()) assert.Contains(t, output.GetStderr(), "execute error") } func TestUpdate_NoUpdateScript(t *testing.T) { // Setup mocks with expectations mockFileSys := MockedFileSys{} actionPathNoExt := filepath.Join(testPackagePath, "update") mockReadAction(t, &mockFileSys, actionPathNoExt, []byte{}, []byte{}, false) mockExec := MockedExec{} mockEnvdetectCollector := &envdetectmocks.CollectorMock{} mockEnvdetectCollector.On("CollectData", mock.Anything).Return(&environmentStub, nil).Once() tracer := trace.NewTracer(log.NewMockLog()) // Instantiate installer with mock inst := Installer{filesysdep: &mockFileSys, execdep: &mockExec, packagePath: testPackagePath, config: contracts.Configuration{OutputS3BucketName: "foo", OutputS3KeyPrefix: "bar"}, envdetectCollector: mockEnvdetectCollector} // Call and validate mock expectations and return value output := inst.Update(tracer, contextMock) mockFileSys.AssertExpectations(t) mockExec.AssertExpectations(t) assert.NotEmpty(t, output.GetStderr()) assert.Contains(t, output.GetStderr(), "missing update script") } // Load specified file from file system func loadFile(t *testing.T, fileName string) (result []byte) { var err error if result, err = ioutil.ReadFile(fileName); err != nil { t.Fatal(err) } return } type MockedFileSys struct { mock.Mock } func (fileMock *MockedFileSys) Exists(filePath string) bool { args := fileMock.Called(filePath) return args.Bool(0) } func (fileMock *MockedFileSys) ReadFile(filename string) ([]byte, error) { args := fileMock.Called(filename) return args.Get(0).([]byte), args.Error(1) } type MockedExec struct { mock.Mock } func (execMock *MockedExec) ParseDocument( context context.T, documentRaw []byte, orchestrationDir string, s3Bucket string, s3KeyPrefix string, messageID string, documentID string, defaultWorkingDirectory string) (pluginsInfo []contracts.PluginState, err error) { args := execMock.Called(context, documentRaw, orchestrationDir, s3Bucket, s3KeyPrefix, messageID, documentID, defaultWorkingDirectory) return args.Get(0).([]contracts.PluginState), args.Error(1) } func (execMock *MockedExec) ExecuteDocument( context context.T, pluginInput []contracts.PluginState, documentID string, documentCreatedDate string, orchestrationDirectory string) (pluginOutputs map[string]*contracts.PluginResult) { args := execMock.Called(context, pluginInput, documentID, documentCreatedDate) return args.Get(0).(map[string]*contracts.PluginResult) }