// Our intent here is to look for array of commands which will be executed as a part of this document and replace the incompatible code. func removeDependencyOnInstanceMetadataForManagedInstance(context context.T, parsedMessage messageContracts.SendCommandPayload) (messageContracts.SendCommandPayload, error) { log := context.Log() var properties []interface{} var parsedDocumentProperties ManagedInstanceDocumentProperties err := jsonutil.Remarshal(parsedMessage.DocumentContent.RuntimeConfig[appconfig.PluginNameAwsRunScript].Properties, &properties) if err != nil { log.Errorf("Invalid format of properties in %v document. error: %v", parsedMessage.DocumentName, err) return parsedMessage, err } // Since 'Properties' is an array and we use only one property block for the above documents, array location '0' of 'Properties' is used. err = jsonutil.Remarshal(properties[0], &parsedDocumentProperties) if err != nil { log.Errorf("Invalid format of properties in %v document. error: %v", parsedMessage.DocumentName, err) return parsedMessage, err } region, err := platform.Region() if err != nil { log.Errorf("Error retrieving agent region. error: %v", err) return parsedMessage, err } // Comment or replace the incompatible code from this document. log.Info("Replacing managed instance incompatible code for AWS SSM Document.") for i, command := range parsedDocumentProperties.RunCommand { if strings.Contains(command, "$metadataLocation = 'http://169.254.169.254/latest/dynamic/instance-identity/document/region'") { parsedDocumentProperties.RunCommand[i] = strings.Replace(command, "$metadataLocation = 'http://169.254.169.254/latest/dynamic/instance-identity/document/region'", "# $metadataLocation = 'http://169.254.169.254/latest/dynamic/instance-identity/document/region' (This is done to make it managed instance compatible)", 1) } if strings.Contains(command, "$metadata = (New-Object Net.WebClient).DownloadString($metadataLocation)") { parsedDocumentProperties.RunCommand[i] = strings.Replace(command, "$metadata = (New-Object Net.WebClient).DownloadString($metadataLocation)", "# $metadata = (New-Object Net.WebClient).DownloadString($metadataLocation) (This is done to make it managed instance compatible)", 1) } if strings.Contains(command, "$region = (ConvertFrom-JSON $metadata).region") { parsedDocumentProperties.RunCommand[i] = strings.Replace(command, "$region = (ConvertFrom-JSON $metadata).region", "$region = '"+region+"'", 1) } } // Plug-in the compatible 'Properties' block back to the document. properties[0] = parsedDocumentProperties var documentProperties interface{} = properties parsedMessage.DocumentContent.RuntimeConfig[appconfig.PluginNameAwsRunScript].Properties = documentProperties // For debug purposes. parsedMessageContent, err := jsonutil.Marshal(parsedMessage) if err != nil { log.Errorf("Error marshalling %v document. error: %v", parsedMessage.DocumentName, err) return parsedMessage, err } log.Debug("ParsedMessage after removing dependency on instance metadata for managed instance is ", jsonutil.Indent(parsedMessageContent)) return parsedMessage, nil }
// testRunCommands tests the runCommands or the runCommandsRawInput method for one testcase. func testRunCommands(t *testing.T, testCase TestCase, rawInput bool) { logger.On("Error", mock.Anything).Return(nil) logger.Infof("test run commands %v", testCase) runCommandTester := func(p *Plugin, mockCancelFlag *task.MockCancelFlag, mockExecuter *executers.MockCommandExecuter, mockS3Uploader *pluginutil.MockDefaultPlugin) { // set expectations setExecuterExpectations(mockExecuter, testCase, mockCancelFlag, p) setS3UploaderExpectations(mockS3Uploader, testCase, p) // call method under test var res contracts.PluginOutput if rawInput { // prepare plugin input var rawPluginInput interface{} err := jsonutil.Remarshal(testCase.Input, &rawPluginInput) assert.Nil(t, err) res = p.runCommandsRawInput(logger, rawPluginInput, orchestrationDirectory, mockCancelFlag, s3BucketName, s3KeyPrefix) } else { res = p.runCommands(logger, testCase.Input, orchestrationDirectory, mockCancelFlag, s3BucketName, s3KeyPrefix) } // assert output is correct (mocked object expectations are tested automatically by testExecution) assert.Equal(t, testCase.Output, res) } testExecution(t, runCommandTester) }
// runCommandsRawInput executes one set of commands and returns their output. // The input is in the default json unmarshal format (e.g. map[string]interface{}). func (p *Plugin) runCommandsRawInput(log log.T, rawPluginInput interface{}, orchestrationDirectory string, cancelFlag task.CancelFlag, outputS3BucketName string, outputS3KeyPrefix string) (out ApplicationPluginOutput) { var pluginInput ApplicationPluginInput err := jsonutil.Remarshal(rawPluginInput, &pluginInput) log.Debugf("Plugin input %v", pluginInput) if err != nil { errorString := fmt.Errorf("Invalid format in plugin properties %v;\nerror %v", rawPluginInput, err) out.MarkAsFailed(log, errorString) return } return p.runCommands(log, pluginInput, orchestrationDirectory, cancelFlag, outputS3BucketName, outputS3KeyPrefix) }
// runCommandsRawInput executes one set of commands and returns their output. // The input is in the default json unmarshal format (e.g. map[string]interface{}). func (p *Plugin) runCommandsRawInput(log log.T, rawPluginInput interface{}, orchestrationDirectory string, cancelFlag task.CancelFlag, outputS3BucketName string, outputS3KeyPrefix string) (out contracts.PluginOutput) { var pluginInput RunCommandPluginInput err := jsonutil.Remarshal(rawPluginInput, &pluginInput) if err != nil { errorString := fmt.Sprintf("Invalid format in plugin properties %v;\nerror %v", rawPluginInput, err) out.Errors = append(out.Errors, errorString) out.Status = contracts.ResultStatusFailed log.Error(errorString) return } return p.runCommands(log, pluginInput, orchestrationDirectory, cancelFlag, outputS3BucketName, outputS3KeyPrefix) }
// LoadParametersAsList returns properties as a list and appropriate PluginResult if error is encountered func LoadParametersAsList(log log.T, prop interface{}) ([]interface{}, contracts.PluginResult) { var properties []interface{} var res contracts.PluginResult if err := jsonutil.Remarshal(prop, &properties); err != nil { log.Errorf("unable to parse plugin configuration") res.Output = "Execution failed because agent is unable to parse plugin configuration" res.Code = 1 res.Status = contracts.ResultStatusFailed } return properties, res }
// testExecute tests the run command plugin's Execute method. func testExecute(t *testing.T, testCase TestCase) { executeTester := func(p *Plugin, mockCancelFlag *task.MockCancelFlag, mockExecuter *executers.MockCommandExecuter, mockS3Uploader *pluginutil.MockDefaultPlugin) { // setup expectations and correct outputs var pluginProperties []interface{} var correctOutputs string mockContext := context.NewMockDefault() // set expectations setCancelFlagExpectations(mockCancelFlag) setExecuterExpectations(mockExecuter, testCase, mockCancelFlag, p) setS3UploaderExpectations(mockS3Uploader, testCase, p) // prepare plugin input var rawPluginInput interface{} err := jsonutil.Remarshal(testCase.Input, &rawPluginInput) assert.Nil(t, err) pluginProperties = append(pluginProperties, rawPluginInput) correctOutputs = testCase.Output.String() //Create messageId which is in the format of aws.ssm.<commandID>.<InstanceID> commandID := uuid.NewV4().String() // call plugin res := p.Execute( mockContext, contracts.Configuration{ Properties: pluginProperties, OutputS3BucketName: s3BucketName, OutputS3KeyPrefix: s3KeyPrefix, OrchestrationDirectory: orchestrationDirectory, BookKeepingFileName: commandID, }, mockCancelFlag) // assert output is correct (mocked object expectations are tested automatically by testExecution) assert.NotNil(t, res.StartDateTime) assert.NotNil(t, res.EndDateTime) assert.Equal(t, correctOutputs, res.Output) // assert that the flag is checked after every set of commands mockCancelFlag.AssertNumberOfCalls(t, "Canceled", 1) } testExecution(t, executeTester) }
// updateAgent downloads the installation packages and update the agent func runUpdateAgent( p *Plugin, config contracts.Configuration, log log.T, manager pluginHelper, util updateutil.T, rawPluginInput interface{}, cancelFlag task.CancelFlag, outputS3BucketName string, outputS3KeyPrefix string, startTime time.Time) (out UpdatePluginOutput) { var pluginInput UpdatePluginInput var err error var context *updateutil.InstanceContext if isUpdateSupported, err := util.IsPlatformSupportedForUpdate(log); err == nil && !isUpdateSupported { out.Failed(log, fmt.Errorf("Unsupported platform for update")) return } if err = jsonutil.Remarshal(rawPluginInput, &pluginInput); err != nil { out.Failed(log, fmt.Errorf("invalid format in plugin properties %v;\nerror %v", rawPluginInput, err)) return } if context, err = util.CreateInstanceContext(log); err != nil { out.Failed(log, err) return } //Use default manifest location is the override is not present if len(pluginInput.Source) == 0 { pluginInput.Source = p.ManifestLocation } //Calculate manifest location base on current instance's region pluginInput.Source = strings.Replace(pluginInput.Source, updateutil.RegionHolder, context.Region, -1) //Calculate updater package name base on agent name pluginInput.UpdaterName = pluginInput.AgentName + updateutil.UpdaterPackageNamePrefix //Generate update output targetVersion := pluginInput.TargetVersion if len(targetVersion) == 0 { targetVersion = "latest" } out.AppendInfo(log, "Updating %v from %v to %v", pluginInput.AgentName, version.Version, targetVersion) //Download manifest file manifest, downloadErr := manager.downloadManifest(log, util, &pluginInput, context, &out) if downloadErr != nil { out.Failed(log, downloadErr) return } //Validate update details noNeedToUpdate := false if noNeedToUpdate, err = manager.validateUpdate(log, &pluginInput, context, manifest, &out); noNeedToUpdate { if err != nil { out.Failed(log, err) } return } //Download updater and retrieve the version number updaterVersion := "" if updaterVersion, err = manager.downloadUpdater( log, util, pluginInput.UpdaterName, manifest, &out, context); err != nil { out.Failed(log, err) return } //Generate update command base on the update detail cmd := "" if cmd, err = manager.generateUpdateCmd(log, manifest, &pluginInput, context, updateutil.UpdaterFilePath(appconfig.UpdaterArtifactsRoot, pluginInput.UpdaterName, updaterVersion), config.MessageId, p.StdoutFileName, p.StderrFileName, outputS3KeyPrefix, outputS3BucketName); err != nil { out.Failed(log, err) return } log.Debugf("Update command %v", cmd) //Save update plugin result to local file, updater will read it during agent update updatePluginResult := &updateutil.UpdatePluginResult{ StandOut: out.Stdout, StartDateTime: startTime, } if err = util.SaveUpdatePluginResult(log, appconfig.UpdaterArtifactsRoot, updatePluginResult); err != nil { out.Failed(log, err) return } // If disk space is not sufficient, fail the update to prevent installation and notify user in output // If loading disk space fails, continue to update (agent update is backed by rollback handler) log.Infof("Checking available disk space ...") if isDiskSpaceSufficient, err := util.IsDiskSpaceSufficientForUpdate(log); err == nil && !isDiskSpaceSufficient { out.Failed(log, errors.New("Insufficient available disk space")) return } log.Infof("Start Installation") log.Infof("Hand over update process to %v", pluginInput.UpdaterName) //Execute updater, hand over the update process workDir := updateutil.UpdateArtifactFolder( appconfig.UpdaterArtifactsRoot, pluginInput.UpdaterName, updaterVersion) if err = util.ExeCommand( log, cmd, workDir, appconfig.UpdaterArtifactsRoot, p.StdoutFileName, p.StderrFileName, true); err != nil { out.Failed(log, err) return } out.Pending() return }