// GetS3Config returns the S3 config used for uploading output files to S3 func GetS3Config() *s3util.Manager { //There are multiple ways of supporting the cross-region upload to S3 bucket: //1) We can specify the url https://s3.amazonaws.com and not specify region in our s3 client. This approach only works in java & .net but not in golang //since it enforces to use region in our client. //2) We can make use of GetBucketLocation API to find the location of S3 bucket. This is a better way to handle this, however it has its own disadvantages: //-> We will have to update the managed policy of AmazonEC2RoleforSSM so that agent will have permissions to make that call. //-> We will still have to notify our customers regarding the change in our IAM policy - such that customers using inline policy will also make the change accordingly. //3) Special behavior for S3 PutObject API for IAD region which is described in detail below. //We have taken the 3rd option - until the changes for the 2nd option is in place. //In our current implementation, we upload a test S3 file and use the error message to determine the bucket's region, //but we do this with region set as "us-east-1". This is because of special behavior of S3 PutObject API: //Only for the endpoint "us-east-1", if the bucket is present in any other region (i.e non IAD bucket) PutObject API will throw an //error of type - AuthorizationHeaderMalformed with a message stating which region is the bucket present. A sample error message looks like: //AuthorizationHeaderMalformed: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'us-west-2' status code: 400, request id: [] //We leverage the above error message to determine the bucket's region, and if there is no error - that means the bucket is indeed in IAD. //Note: The above behavior only exists for IAD endpoint (special endpoint for S3) - not just any other region. //For other region endpoints, you get a BucketRegionError which is not useful for us in determining where the bucket is present. //Revisit this if S3 ensures the PutObject API behavior consistent over all endpoints - in which case - instead of using IAD endpoint, //we can then pick the endpoint from meta-data instead. awsConfig := sdkutil.AwsConfig() if region, err := platform.Region(); err == nil && region == s3Bjs { awsConfig.Endpoint = &s3BjsEndpoint awsConfig.Region = &s3Bjs } else { awsConfig.Endpoint = &s3StandardEndpoint awsConfig.Region = &S3RegionUSStandard } s3 := s3.New(session.New(awsConfig)) return s3util.NewManager(s3) }
// AwsConfig returns the default aws.Config object while the appropriate // credentials. Callers should override returned config properties with any // values they want for service specific overrides. func AwsConfig() (awsConfig *aws.Config) { // create default config awsConfig = &aws.Config{ Retryer: newRetryer(), SleepDelay: sleepDelay, } // update region from platform region, err := platform.Region() if region != "" { awsConfig.Region = ®ion } // load managed credentials if applicable if isManaged, err := registration.HasManagedInstancesCredentials(); isManaged && err == nil { awsConfig.Credentials = rolecreds.ManagedInstanceCredentialsInstance() return } // look for profile credentials appConfig, err := appconfig.Config(false) if err == nil { creds, _ := appConfig.ProfileCredentials() if creds != nil { awsConfig.Credentials = creds } } return }
// NewCoreManager creates a new core plugin manager. func NewCoreManager(instanceIdPtr *string, regionPtr *string, log logger.T) (cm *CoreManager, err error) { // initialize appconfig var config appconfig.SsmagentConfig if config, err = appconfig.Config(false); err != nil { log.Errorf("Could not load config file: %v", err) return } // initialize region if *regionPtr != "" { if err = platform.SetRegion(*regionPtr); err != nil { log.Errorf("error occured setting the region, %v", err) return } } var region string if region, err = platform.Region(); err != nil { log.Errorf("error fetching the region, %v", err) return } log.Debug("Using region:", region) // initialize instance ID if *instanceIdPtr != "" { if err = platform.SetInstanceID(*instanceIdPtr); err != nil { log.Errorf("error occured setting the instance ID, %v", err) return } } var instanceId string if instanceId, err = platform.InstanceID(); err != nil { log.Errorf("error fetching the instanceID, %v", err) return } log.Debug("Using instanceID:", instanceId) if err = fileutil.HardenDataFolder(); err != nil { log.Errorf("error initializing SSM data folder with hardened ACL, %v", err) return } //Initialize all folders where interim states of executing commands will be stored. if !initializeBookkeepingLocations(log, instanceId) { log.Error("unable to initialize. Exiting") return } context := context.Default(log, config).With("[instanceID=" + instanceId + "]") corePlugins := coreplugins.RegisteredCorePlugins(context) return &CoreManager{ context: context, corePlugins: *corePlugins, }, nil }
// 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 }
// GetUpdatePluginConfig returns the default values for the update plugin func GetUpdatePluginConfig(context context.T) UpdatePluginConfig { log := context.Log() region, err := platform.Region() if err != nil { log.Errorf("Error retrieving agent region in update plugin config. error: %v", err) } var manifestUrl string if region == "cn-north-1" { manifestUrl = "https://s3.cn-north-1.amazonaws.com.cn/amazon-ssm-cn-north-1/ssm-agent-manifest.json" } else { manifestUrl = "https://amazon-ssm-{Region}.s3.amazonaws.com/ssm-agent-manifest.json" } return UpdatePluginConfig{ ManifestLocation: manifestUrl, StdoutFileName: "stdout", StderrFileName: "stderr", MaxStdoutLength: 2500, MaxStderrLength: 2500, OutputTruncatedSuffix: "--output truncated--", } }
// UploadOutputToS3Bucket uploads outputs (if any) to s3 func (p *DefaultPlugin) UploadOutputToS3Bucket(log log.T, pluginID string, orchestrationDir string, outputS3BucketName string, outputS3KeyPrefix string, useTempDirectory bool, tempDir string, Stdout string, Stderr string) []string { var uploadOutputToS3BucketErrors []string if outputS3BucketName != "" { uploadOutputsToS3 := func() { uploadToS3 := true var testUploadError error if region, err := platform.Region(); err == nil && region != s3Bjs { p.Uploader.SetS3ClientRegion(S3RegionUSStandard) } log.Infof("uploading a test file to s3 bucket - %v , s3 key - %v with S3Client using region endpoint - %v", outputS3BucketName, outputS3KeyPrefix, p.Uploader.GetS3ClientRegion()) testUploadError = p.Uploader.UploadS3TestFile(log, outputS3BucketName, outputS3KeyPrefix) if testUploadError != nil { //Check if the error is related to Access Denied - i.e missing permissions if p.Uploader.IsS3ErrorRelatedToAccessDenied(testUploadError.Error()) { log.Debugf("encountered access denied related error - can't upload to S3 due to missing permissions -%v", testUploadError.Error()) uploadToS3 = false //since we don't have permissions - no S3 calls will go through no matter what } else if p.Uploader.IsS3ErrorRelatedToWrongBucketRegion(testUploadError.Error()) { //check if error is related to different bucket region log.Debugf("encountered error related to wrong bucket region while uploading test file to S3 - %v. parsing the message to get expected region", testUploadError.Error()) expectedBucketRegion := p.Uploader.GetS3BucketRegionFromErrorMsg(log, testUploadError.Error()) //set the region to expectedBucketRegion p.Uploader.SetS3ClientRegion(expectedBucketRegion) } else { log.Debugf("encountered unexpected error while uploading test file to S3 - %v, no need to modify s3client", testUploadError.Error()) } } else { //there were no errors while uploading a test file to S3 - our s3client should continue to use "us-east-1" log.Debugf("there were no errors while uploading a test file to S3 in region - %v. S3 client will continue to use region - %v", S3RegionUSStandard, p.Uploader.GetS3ClientRegion()) } if uploadToS3 { log.Infof("uploading logs to S3 with client configured to use region - %v", p.Uploader.GetS3ClientRegion()) if useTempDirectory { // delete temp directory once we're done defer DeleteDirectory(log, tempDir) } if Stdout != "" { localPath := filepath.Join(orchestrationDir, p.StdoutFileName) s3Key := path.Join(outputS3KeyPrefix, pluginID, p.StdoutFileName) log.Debugf("Uploading %v to s3://%v/%v", localPath, outputS3BucketName, s3Key) err := p.Uploader.S3Upload(outputS3BucketName, s3Key, localPath) if err != nil { log.Errorf("failed uploading %v to s3://%v/%v err:%v", localPath, outputS3BucketName, s3Key, err) if p.UploadToS3Sync { // if we are in synchronous mode, we can also return the error uploadOutputToS3BucketErrors = append(uploadOutputToS3BucketErrors, err.Error()) } } } if Stderr != "" { localPath := filepath.Join(orchestrationDir, p.StderrFileName) s3Key := path.Join(outputS3KeyPrefix, pluginID, p.StderrFileName) log.Debugf("Uploading %v to s3://%v/%v", localPath, outputS3BucketName, s3Key) err := p.Uploader.S3Upload(outputS3BucketName, s3Key, localPath) if err != nil { log.Errorf("failed uploading %v to s3://%v/%v err:%v", localPath, outputS3BucketName, s3Key, err) if p.UploadToS3Sync { // if we are in synchronous mode, we can also return the error uploadOutputToS3BucketErrors = append(uploadOutputToS3BucketErrors, err.Error()) } } } } else { //TODO:Bubble this up to engine - so that document level status reply can be sent stating no permissions to perform S3 upload - similar to ec2config } } if p.UploadToS3Sync { uploadOutputsToS3() } else { go uploadOutputsToS3() } } //return out.Errors return uploadOutputToS3BucketErrors }