// 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 rundocument implements the aws:runDocument plugin package rundocument import ( "fmt" "io/ioutil" "path/filepath" "testing" "time" "github.com/aws/amazon-ssm-agent/agent/mocks/context" "github.com/aws/amazon-ssm-agent/agent/mocks/log" taskmocks "github.com/aws/amazon-ssm-agent/agent/mocks/task" "github.com/aws/amazon-ssm-agent/agent/plugins/rundocument/mocks/rundocument" ssmsvc "github.com/aws/amazon-ssm-agent/agent/ssm/mocks/ssm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/aws/amazon-ssm-agent/agent/contracts" filemock "github.com/aws/amazon-ssm-agent/agent/fileutil/filemanager/mock" iohandlermocks "github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/iohandler/mock" executermocks "github.com/aws/amazon-ssm-agent/agent/framework/processor/executer/mock" "github.com/aws/amazon-ssm-agent/agent/task" "github.com/aws/aws-sdk-go/service/ssm" ) // instanceMock type InstanceMock struct { mock.Mock } // InstanceID mocks implementation for InstanceID func (m *InstanceMock) InstanceID() (string, error) { return "", nil } var logMock = log.NewMockLog() var contextMock = context.NewMockDefault() var plugin = contracts.PluginState{} func TestReadFileContents(t *testing.T) { fileMock := &filemock.FileSystemMock{} destinationDir := "destination" fileMock.On("ReadFile", destinationDir).Return("content", nil) rawFile, err := readFileContents(logMock, fileMock, destinationDir) assert.NoError(t, err) assert.Equal(t, []byte("content"), rawFile) fileMock.AssertExpectations(t) } func TestReadFileContents_Fail(t *testing.T) { fileMock := &filemock.FileSystemMock{} destinationDir := "destination" fileMock.On("ReadFile", destinationDir).Return("content", fmt.Errorf("Error")) _, err := readFileContents(logMock, fileMock, destinationDir) assert.Error(t, err) fileMock.AssertExpectations(t) } func TestExecDocumentImpl_ExecuteDocumentFailure(t *testing.T) { //Expected out isFail because plugin output is nil documentId := "documentId" var pluginInput []contracts.PluginState pluginInput = append(pluginInput, plugin) execMock := executermocks.NewMockExecuter() docResultChan := make(chan contracts.DocumentResult) execMock.On("Run", mock.AnythingOfType("*task.ChanneledCancelFlag"), mock.AnythingOfType("*executer.DocumentFileStore")).Return(docResultChan) exec := ExecDocumentImpl{ DocExecutor: execMock, } conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") _, err := exec.ExecuteDocument(conf, contextMock, pluginInput, documentId, "time") assert.NoError(t, err) } func TestExecDocumentImpl_ExecuteDocumentSuccess(t *testing.T) { documentId := "documentId" var pluginInput []contracts.PluginState pluginInput = append(pluginInput, plugin) pluginRes := make(map[string]*contracts.PluginResult) pluginResult := contracts.PluginResult{ PluginName: "aws:runDocument", Status: contracts.ResultStatusSuccess, StandardOutput: "out", } pluginRes["aws:runDocument"] = &pluginResult execMock := executermocks.NewMockExecuter() docResultChan := make(chan contracts.DocumentResult) execMock.On("Run", mock.AnythingOfType("*task.ChanneledCancelFlag"), mock.AnythingOfType("*executer.DocumentFileStore")).Return(docResultChan) exec := ExecDocumentImpl{ DocExecutor: execMock, } conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") _, err := exec.ExecuteDocument(conf, contextMock, pluginInput, documentId, "time") assert.NoError(t, err) } func TestExecDocumentImpl_ExecuteDocumentWithMultiplePlugin(t *testing.T) { documentId := "documentId" conf := contracts.Configuration{ OrchestrationDirectory: "orch", OutputS3BucketName: "bucket", OutputS3KeyPrefix: "prefix", MessageId: "1234567890", PluginID: "aws:runShellScript", DefaultWorkingDirectory: "directory", PluginName: "aws:runShellScript", } var pluginInput []contracts.PluginState pluginInput = append(pluginInput, plugin) execMock := executermocks.NewMockExecuter() docResultChan := make(chan contracts.DocumentResult) execMock.On("Run", mock.AnythingOfType("*task.ChanneledCancelFlag"), mock.AnythingOfType("*executer.DocumentFileStore")).Return(docResultChan) exec := ExecDocumentImpl{ DocExecutor: execMock, } _, err := exec.ExecuteDocument(conf, contextMock, pluginInput, documentId, "time") assert.NoError(t, err) } func TestExecutePlugin_PrepareDocumentForExecution(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} parameters := make(map[string]interface{}) conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") content := `{ "key" : "value" }` fileMock.On("ReadFile", "document/name.json").Return(content, nil) execMock.On("ParseDocument", contextMock, []byte(content), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } _, err := p.prepareDocumentForExecution(logMock, "document/name.json", conf, "") assert.NoError(t, err) fileMock.AssertExpectations(t) execMock.AssertExpectations(t) } func TestExecutePlugin_PrepareDocumentForExecutionFail(t *testing.T) { execMock := rundocument.NewExecMock() localFileMock := filemock.FileSystemMock{} conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") localFileMock.On("ReadFile", "document/name.json").Return("", fmt.Errorf("File is empty!")) p := Plugin{ context: contextMock, filesys: &localFileMock, execDoc: &execMock, } _, err := p.prepareDocumentForExecution(logMock, "document/name.json", conf, "") assert.Error(t, err) assert.Equal(t, fmt.Errorf("File is empty!"), err) localFileMock.AssertExpectations(t) execMock.AssertExpectations(t) } func TestExecuteImpl_PrepareDocumentForExecutionParametersYAML(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} params := ` param1: hello param2: world` parameters := make(map[string]interface{}) parameters["param1"] = "hello" parameters["param2"] = "world" conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") fileMock.On("ReadFile", "document/doc-name.json").Return("content", nil) execMock.On("ParseDocument", contextMock, []byte("content"), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } _, err := p.prepareDocumentForExecution(logMock, "document/doc-name.json", conf, params) assert.NoError(t, err) fileMock.AssertExpectations(t) execMock.AssertExpectations(t) } func TestExecuteImpl_PrepareDocumentForExecutionParametersJSON(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} params := `{ "param1":"hello", "param2":"world" }` parameters := make(map[string]interface{}) parameters["param1"] = "hello" parameters["param2"] = "world" conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") fileMock.On("ReadFile", "document/doc-name.yaml").Return("content", nil) execMock.On("ParseDocument", contextMock, []byte("content"), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } _, err := p.prepareDocumentForExecution(logMock, "document/doc-name.yaml", conf, params) assert.NoError(t, err) fileMock.AssertExpectations(t) execMock.AssertExpectations(t) } func TestPlugin_RunDocumentMaxDepthExceeded(t *testing.T) { // Test to check if the max depth code works in the fail case execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} mockIOHandler := new(iohandlermocks.MockIOHandler) mockplugin := MockDefaultPlugin{} conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") var input RunDocumentPluginInput input.DocumentType = "LocalPath" input.DocumentPath = filepath.Join("var", "tmp", "docLocation", "docname.json") conf.Properties = &input var executionDepth interface{} executionDepth = createStubExecutionDepth(4) conf.Settings = executionDepth mockIOHandler.On("MarkAsFailed", fmt.Errorf("Maximum depth for document execution exceeded. Maximum depth permitted - 3 and current depth - 5")).Return() p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } p.execute(conf, createMockCancelFlag(), mockIOHandler) execMock.AssertExpectations(t) mockplugin.AssertExpectations(t) fileMock.AssertExpectations(t) mockIOHandler.AssertExpectations(t) } func TestPlugin_RunDocument(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} mockIOHandler := new(iohandlermocks.MockIOHandler) mockplugin := MockDefaultPlugin{} conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") var input RunDocumentPluginInput input.DocumentType = LocalPathType input.DocumentPath = filepath.Join("var", "tmp", "docLocation", "docname.json") conf.Properties = &input resChan := make(chan contracts.DocumentResult) pluginRes := contracts.PluginResult{ PluginID: "aws:runDocument", PluginName: "aws:runDocument", Status: contracts.ResultStatusSuccess, Code: 0, } pluginResults := make(map[string]*contracts.PluginResult) pluginResults[pluginRes.PluginID] = &pluginRes go func() { res := contracts.DocumentResult{ LastPlugin: "", Status: contracts.ResultStatusSuccess, PluginResults: pluginResults, } resChan <- res close(resChan) }() parameters := make(map[string]interface{}) content := "content" plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} fileMock.On("ReadFile", filepath.Join("orch", "downloads", "var", "tmp", "docLocation", "docname.json")).Return(content, nil) execMock.On("ParseDocument", contextMock, []byte(content), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) execMock.On("ExecuteDocument", contextMock, plugins, conf.BookKeepingFileName, mock.Anything).Return(resChan, nil) mockIOHandler.On("GetStatus").Return(contracts.ResultStatusSuccess) mockIOHandler.On("SetStatus", contracts.ResultStatusSuccess).Return() p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } p.execute(conf, createMockCancelFlag(), mockIOHandler) execMock.AssertExpectations(t) mockplugin.AssertExpectations(t) fileMock.AssertExpectations(t) mockIOHandler.AssertExpectations(t) } func TestPlugin_RunDocumentFromSSMDocument(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} mockIOHandler := new(iohandlermocks.MockIOHandler) ssmMock := ssmsvc.NewMockDefault() content := "content" docResponse := ssm.GetDocumentOutput{ Content: &content, } conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} parameters := make(map[string]interface{}) resChan := make(chan contracts.DocumentResult) pluginRes := contracts.PluginResult{ PluginID: "aws:runDocument", PluginName: "aws:runDocument", Status: contracts.ResultStatusSuccess, Code: 0, } pluginResults := make(map[string]*contracts.PluginResult) pluginResults[pluginRes.PluginID] = &pluginRes go func() { res := contracts.DocumentResult{ LastPlugin: "", Status: contracts.ResultStatusSuccess, PluginResults: pluginResults, } resChan <- res close(resChan) }() ssmMock.On("GetDocument", contextMock.Log(), "RunShellScript", "10").Return(&docResponse, nil) fileMock.On("MakeDirs", filepath.Join("orch", "downloads")).Return(nil) fileMock.On("WriteFile", filepath.Join("orch", "downloads", "RunShellScript.json"), content).Return(nil) fileMock.On("ReadFile", filepath.Join("orch", "downloads", "RunShellScript.json")).Return(content, nil) execMock.On("ParseDocument", contextMock, []byte(content), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) execMock.On("ExecuteDocument", contextMock, plugins, conf.BookKeepingFileName, mock.Anything).Return(resChan, nil) mockIOHandler.On("GetStatus").Return(contracts.ResultStatusSuccess) mockIOHandler.On("SetStatus", contracts.ResultStatusSuccess).Return() var input RunDocumentPluginInput input.DocumentType = "SSMDocument" input.DocumentPath = "RunShellScript:10" conf.Properties = &input p := Plugin{ context: contextMock, filesys: &fileMock, ssmSvc: ssmMock, execDoc: &execMock, } p.runDocument(&input, conf, mockIOHandler) execMock.AssertExpectations(t) fileMock.AssertExpectations(t) mockIOHandler.AssertExpectations(t) ssmMock.AssertExpectations(t) } func TestPlugin_RunDocumentFromAbsLocalPath(t *testing.T) { execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} mockIOHandler := new(iohandlermocks.MockIOHandler) content := "content" conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") plugin := contracts.PluginState{} plugins := []contracts.PluginState{plugin} pluginRes := contracts.PluginResult{ PluginID: "aws:runDocument", PluginName: "aws:runDocument", Status: contracts.ResultStatusSuccess, Code: 0, } pluginResults := make(map[string]*contracts.PluginResult) pluginResults[pluginRes.PluginID] = &pluginRes resChan := make(chan contracts.DocumentResult) go func() { res := contracts.DocumentResult{ LastPlugin: "", Status: contracts.ResultStatusSuccess, PluginResults: pluginResults, } resChan <- res close(resChan) }() parameters := make(map[string]interface{}) fileMock.On("ReadFile", filepath.Join(rootAbsPath, "tmp", "document", "docName.json")).Return(content, nil) execMock.On("ParseDocument", contextMock, []byte(content), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, parameters).Return(plugins, nil) execMock.On("ExecuteDocument", contextMock, plugins, conf.BookKeepingFileName, mock.Anything).Return(resChan, nil) mockIOHandler.On("GetStatus").Return(contracts.ResultStatusSuccess) mockIOHandler.On("SetStatus", contracts.ResultStatusSuccess).Return() var input RunDocumentPluginInput input.DocumentType = "LocalPath" input.DocumentPath = filepath.Join(rootAbsPath, "tmp", "document", "docName.json") conf.Properties = &input p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, } p.runDocument(&input, conf, mockIOHandler) execMock.AssertExpectations(t) fileMock.AssertExpectations(t) mockIOHandler.AssertExpectations(t) } func TestName(t *testing.T) { assert.Equal(t, "aws:runDocument", Name()) } func TestExecDocumentImpl_ParseDocumentYAML(t *testing.T) { yamlDoc := loadFile(t, "testdata/yamldoc.yaml") conf := contracts.Configuration{ OrchestrationDirectory: "orch", OutputS3BucketName: "bucket", OutputS3KeyPrefix: "prefix", MessageId: "1234-1234-1234", PluginID: "aws:runScript", DefaultWorkingDirectory: "directory", PluginName: "aws:runScript", } var exec ExecDocumentImpl var params map[string]interface{} pluginsInfo, err := exec.ParseDocument(contextMock, []byte(yamlDoc), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, params) assert.NoError(t, err) for _, plugin := range pluginsInfo { assert.NotEqual(t, plugin.Configuration, conf) assert.Equal(t, plugin.Id, conf.PluginID) assert.Equal(t, plugin.Name, conf.PluginName) assert.NotEqual(t, nil, plugin.Configuration.Properties) } } func TestExecDocumentImpl_ParseDocumentJSON(t *testing.T) { jsonDoc := loadFile(t, "testdata/jsondoc.json") conf := contracts.Configuration{ OrchestrationDirectory: "orch", OutputS3BucketName: "bucket", OutputS3KeyPrefix: "prefix", MessageId: "1234-1234-1234", PluginID: "aws:runScript", DefaultWorkingDirectory: "directory", PluginName: "aws:runScript", } var exec ExecDocumentImpl var params map[string]interface{} pluginsInfo, err := exec.ParseDocument(contextMock, []byte(jsonDoc), conf.OrchestrationDirectory, conf.OutputS3BucketName, conf.OutputS3KeyPrefix, conf.MessageId, conf.PluginID, conf.DefaultWorkingDirectory, params) assert.NoError(t, err) for _, plugin := range pluginsInfo { assert.NotEqual(t, plugin.Configuration, conf) assert.Equal(t, plugin.Id, conf.PluginID) assert.Equal(t, plugin.Name, conf.PluginName) assert.NotEqual(t, nil, plugin.Configuration.Properties) } } func TestValidateInput_NoDocumentType(t *testing.T) { input := RunDocumentPluginInput{} result, err := validateInput(&input) assert.False(t, result) assert.Error(t, err) assert.Contains(t, err.Error(), "Document Type must be specified to either by SSMDocument or LocalPath.") } func TestValidateInput_UnknownDocumentType(t *testing.T) { input := RunDocumentPluginInput{} input.DocumentType = "unknown" result, err := validateInput(&input) assert.False(t, result) assert.Error(t, err) assert.Contains(t, err.Error(), "Document type specified in invalid") } func TestValidateInput_EmptyDocumentPath(t *testing.T) { input := RunDocumentPluginInput{} input.DocumentType = LocalPathType result, err := validateInput(&input) assert.False(t, result) assert.Error(t, err) assert.Contains(t, err.Error(), "Document Path must be provided") } func TestParseAndValidateInput_NoInput(t *testing.T) { rawPluginInput := "" _, err := parseAndValidateInput(rawPluginInput) assert.Error(t, err) } func TestDownloadDocumentFromSSM_ARNName(t *testing.T) { conf := createStubConfiguration("orch", "bucket", "prefix", "1234-1234-1234", "directory") input := RunDocumentPluginInput{} input.DocumentType = SSMDocumentType input.DocumentPath = "arn:aws:ssm:us-east-1:1234567890:document/mySharedDocument:10" execMock := rundocument.NewExecMock() fileMock := filemock.FileSystemMock{} ssmMock := ssmsvc.NewMockDefault() content := "content" docResponse := ssm.GetDocumentOutput{ Content: &content, } ssmMock.On("GetDocument", contextMock.Log(), "arn:aws:ssm:us-east-1:1234567890:document/mySharedDocument", "10").Return(&docResponse, nil) fileMock.On("MakeDirs", filepath.Join("orch", "downloads")).Return(nil) fileMock.On("WriteFile", filepath.Join("orch", "downloads", "mySharedDocument.json"), content).Return(nil) p := Plugin{ context: contextMock, filesys: &fileMock, execDoc: &execMock, ssmSvc: ssmMock, } pathToFile, err := p.downloadDocumentFromSSM(contextMock.Log(), conf, &input) assert.NoError(t, err) ssmMock.AssertExpectations(t) execMock.AssertExpectations(t) fileMock.AssertExpectations(t) assert.Equal(t, pathToFile, filepath.Join("orch", "downloads", "mySharedDocument.json")) } func createStubExecutionDepth(depth int) *ExecutePluginDepth { currentDepth := ExecutePluginDepth{} currentDepth.executeCommandDepth = depth return ¤tDepth } func createStubConfiguration(orch, bucket, prefix, message, dir string) contracts.Configuration { return contracts.Configuration{ OrchestrationDirectory: orch, OutputS3BucketName: bucket, OutputS3KeyPrefix: prefix, MessageId: message, PluginID: "aws:runDocument", DefaultWorkingDirectory: dir, } } // MockDefaultPlugin mocks the default plugin. type MockDefaultPlugin struct { mock.Mock } func createMockCancelFlag() task.CancelFlag { mockCancelFlag := new(taskmocks.MockCancelFlag) // Setup mocks mockCancelFlag.On("Canceled").Return(false) mockCancelFlag.On("ShutDown").Return(false) mockCancelFlag.On("Wait").Return(false).After(100 * time.Millisecond) return mockCancelFlag } func loadFile(t *testing.T, fileName string) (result []byte) { result, err := ioutil.ReadFile(fileName) if err != nil { t.Fatal(err) } return }