// 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 ( "errors" "fmt" "os" "path/filepath" "time" "github.com/aws/amazon-ssm-agent/agent/context" "github.com/aws/amazon-ssm-agent/agent/contracts" "github.com/aws/amazon-ssm-agent/agent/fileutil" "github.com/aws/amazon-ssm-agent/agent/jsonutil" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/envdetect" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace" "github.com/aws/amazon-ssm-agent/agent/times" ) type Installer struct { filesysdep fileSysDep execdep execDep packageName string version string additionalArguments string packagePath string config contracts.Configuration // TODO:MF: See if we can use a smaller struct that has just the things we need envdetectCollector envdetect.Collector } type ActionType uint8 const ( ACTION_TYPE_SH ActionType = iota ACTION_TYPE_PS1 ActionType = iota ACTION_INSTALL = "install" ACTION_UPDATE = "update" ACTION_VALIDATE = "validate" ACTION_UNINSTALL = "uninstall" ) type Action struct { actionName string filepath string actionType ActionType } func New(packageName string, version string, additionalArguments string, packagePath string, configuration contracts.Configuration, envdetectCollector envdetect.Collector) *Installer { return &Installer{ filesysdep: &fileSysDepImp{}, execdep: &execDepImp{}, packageName: packageName, version: version, additionalArguments: additionalArguments, packagePath: packagePath, config: configuration, envdetectCollector: envdetectCollector, } } func (inst *Installer) Install(tracer trace.Tracer, context context.T) contracts.PluginOutputter { return inst.executeAction(tracer, context, ACTION_INSTALL) } func (inst *Installer) Update(tracer trace.Tracer, context context.T) contracts.PluginOutputter { return inst.executeAction(tracer, context, ACTION_UPDATE) } func (inst *Installer) Uninstall(tracer trace.Tracer, context context.T) contracts.PluginOutputter { return inst.executeAction(tracer, context, ACTION_UNINSTALL) } func (inst *Installer) Validate(tracer trace.Tracer, context context.T) contracts.PluginOutputter { return inst.executeAction(tracer, context, ACTION_VALIDATE) } func (inst *Installer) Version() string { return inst.version } func (inst *Installer) PackageName() string { return inst.packageName } // executeAction will execute the installer scripts if they exist. func (inst *Installer) executeAction(tracer trace.Tracer, context context.T, actionName string) contracts.PluginOutputter { exectrace := tracer.BeginSection(fmt.Sprintf("execute action: %s", actionName)) output := &trace.PluginOutputTrace{Tracer: tracer} output.SetStatus(contracts.ResultStatusSuccess) exists, pluginsInfo, _, orchestrationDir, err := inst.readAction(tracer, context, actionName) if exists { if err != nil { exectrace.WithError(err) output.MarkAsFailed(nil, nil) } exectrace.AppendInfof("Initiating %v %v %v", inst.packageName, inst.version, actionName) inst.executeDocument(tracer, context, actionName, orchestrationDir, pluginsInfo, output) } else if actionName == ACTION_UPDATE { // Only fail if the action is update and there's no corresponding scripts. // 1) Validate is an internal action, hence we do not expect update script. // 2) Install and uninstall scripts are validated at Distributor creation time. // 3) Update script is optional at Distributor creation time, but is deep validated here if true update is invoked. err := fmt.Errorf("missing update script required for in-place update\nPlease create a Distributor package with update script and retry the install with 'In-place update' installation type, or choose 'Uninstall and reinstall' installation type") exectrace.WithError(err) output.MarkAsFailed(nil, err) } exectrace.End() return output } // getActionPath is a helper function that builds the path to an action document file func (inst *Installer) getActionPath(actionName string, extension string) string { return filepath.Join(inst.packagePath, fmt.Sprintf("%v.%v", actionName, extension)) } func (inst *Installer) readScriptAction(action *Action, workingDir string, orchestrationDir string, pluginName string, runCommand []interface{}, envVars map[string]string) (pluginsInfo []contracts.PluginState, err error) { pluginsInfo = []contracts.PluginState{} pluginFullName := fmt.Sprintf("aws:%v", pluginName) var s3Prefix string if inst.config.OutputS3BucketName != "" { s3Prefix = fileutil.BuildS3Path(inst.config.OutputS3KeyPrefix, inst.config.PluginID, action.actionName, pluginFullName) } inputs := make(map[string]interface{}) inputs["workingDirectory"] = workingDir inputs["runCommand"] = runCommand inputs["environment"] = envVars config := contracts.Configuration{ Settings: nil, Properties: inputs, OutputS3BucketName: inst.config.OutputS3BucketName, OutputS3KeyPrefix: s3Prefix, OrchestrationDirectory: orchestrationDir, MessageId: inst.config.MessageId, BookKeepingFileName: inst.config.BookKeepingFileName, PluginName: pluginFullName, PluginID: inst.version, Preconditions: make(map[string][]contracts.PreconditionArgument), IsPreconditionEnabled: false, DefaultWorkingDirectory: workingDir, } var plugin contracts.PluginState plugin.Configuration = config plugin.Id = config.PluginID plugin.Name = config.PluginName pluginsInfo = append(pluginsInfo, plugin) return pluginsInfo, nil } // readShAction turns an sh action into a set of SSM Document Plugins to execute func (inst *Installer) readShAction(context context.T, action *Action, workingDir string, orchestrationDir string, envVars map[string]string) (pluginsInfo []contracts.PluginState, err error) { if action.actionType != ACTION_TYPE_SH { return nil, fmt.Errorf("Internal error") } runCommand := []interface{}{} runCommand = append(runCommand, fmt.Sprintf("echo Running sh %v.sh", action.actionName)) runCommand = append(runCommand, fmt.Sprintf("sh %v.sh", action.actionName)) return inst.readScriptAction(action, workingDir, orchestrationDir, "runShellScript", runCommand, envVars) } // readPs1Action turns an ps1 action into a set of SSM Document Plugins to execute func (inst *Installer) readPs1Action(context context.T, action *Action, workingDir string, orchestrationDir string, envVars map[string]string) (pluginsInfo []contracts.PluginState, err error) { if action.actionType != ACTION_TYPE_PS1 { return nil, fmt.Errorf("Internal error") } runCommand := []interface{}{} runCommand = append(runCommand, fmt.Sprintf("echo 'Running %v.ps1'", action.actionName)) runCommand = append(runCommand, fmt.Sprintf(".\\%v.ps1; exit $LASTEXITCODE", action.actionName)) return inst.readScriptAction(action, workingDir, orchestrationDir, "runPowerShellScript", runCommand, envVars) } // resolveAction checks if there are multiple installer files for the same action type // and returns false and nil error if the file does not exist. func (inst *Installer) resolveAction(tracer trace.Tracer, actionName string) (exists bool, action *Action, err error) { actionPathSh := inst.getActionPath(actionName, "sh") actionPathPs1 := inst.getActionPath(actionName, "ps1") actionPathExistsSh := inst.filesysdep.Exists(actionPathSh) actionPathExistsPs1 := inst.filesysdep.Exists(actionPathPs1) countExists := 0 actionTemp := &Action{} if actionPathExistsSh { countExists += 1 actionTemp.actionName = actionName actionTemp.actionType = ACTION_TYPE_SH actionTemp.filepath = actionPathSh } if actionPathExistsPs1 { countExists += 1 actionTemp.actionName = actionName actionTemp.actionType = ACTION_TYPE_PS1 actionTemp.filepath = actionPathPs1 } if countExists > 1 { err = fmt.Errorf("%v has more than one implementation (sh, ps1, json)", actionName) tracer.CurrentTrace().WithError(err) return true, nil, err } else if countExists == 1 { return true, actionTemp, nil } return false, nil, nil } func (inst *Installer) getEnvVars(actionName string, context context.T) (envVars map[string]string, err error) { envVars = make(map[string]string) envVars["BWS_ACTION_NAME"] = actionName // Copy proxy settings from the environment envVars["https_proxy"] = os.Getenv("https_proxy") envVars["http_proxy"] = os.Getenv("http_proxy") envVars["no_proxy"] = os.Getenv("no_proxy") env, err := inst.envdetectCollector.CollectData(context) if err != nil { return envVars, fmt.Errorf("failed to collect data: %v", err) } // (Some of these are already available to script as AWS_SSM_INSTANCE_ID and AWS_SSM_REGION_NAME) envVars["BWS_PLATFORM_NAME"] = env.OperatingSystem.Platform envVars["BWS_PLATFORM_VERSION"] = env.OperatingSystem.PlatformVersion envVars["BWS_PLATFORM_FAMILY"] = env.OperatingSystem.PlatformFamily envVars["BWS_ARCHITECTURE"] = env.OperatingSystem.Architecture envVars["BWS_INIT_SYSTEM"] = env.OperatingSystem.InitSystem envVars["BWS_PACKAGE_MANAGER"] = env.OperatingSystem.PackageManager envVars["BWS_INSTANCE_ID"] = env.Ec2Infrastructure.InstanceID envVars["BWS_INSTANCE_TYPE"] = env.Ec2Infrastructure.InstanceType envVars["BWS_REGION"] = env.Ec2Infrastructure.Region envVars["BWS_ACCOUNT_ID"] = env.Ec2Infrastructure.AccountID envVars["BWS_AVAILABILITY_ZONE"] = env.Ec2Infrastructure.AvailabilityZone // Append validated additionalArguments map to environment variables in order to be passed to script execution if inst.additionalArguments != "" { var argumentMap map[string]string jsonutil.Unmarshal(inst.additionalArguments, &argumentMap) for k, v := range argumentMap { envVars[k] = v } } return envVars, err } // readAction returns a JSON document describing a management action and its working directory, or an empty string // if there is nothing to do for a given action func (inst *Installer) readAction(tracer trace.Tracer, context context.T, actionName string) (exists bool, pluginsInfo []contracts.PluginState, workingDir string, orchestrationDir string, err error) { // TODO: Split into linux and windows var action *Action if exists, action, err = inst.resolveAction(tracer, actionName); !exists || action == nil || err != nil { // If the action file does not exist (for eg validate) then this method will return here, with no error. // It could also return if there is an error return exists, nil, "", "", err } workingDir = inst.packagePath orchestrationDir = filepath.Join(inst.config.OrchestrationDirectory, actionName) if ACTION_TYPE_SH != action.actionType && ACTION_TYPE_PS1 != action.actionType { return exists, nil, "", "", fmt.Errorf("Internal error. Unknown actionType %v", action.actionType) } var envVars map[string]string if envVars, err = inst.getEnvVars(actionName, context); err != nil { return exists, nil, "", "", err } if action.actionType == ACTION_TYPE_SH { if pluginsInfo, err = inst.readShAction(context, action, workingDir, orchestrationDir, envVars); err != nil { return exists, nil, "", "", err } } else { if pluginsInfo, err = inst.readPs1Action(context, action, workingDir, orchestrationDir, envVars); err != nil { return exists, nil, "", "", err } } return exists, pluginsInfo, workingDir, orchestrationDir, nil } // executeDocument executes a command document as a sub-document of the current command and returns the result func (inst *Installer) executeDocument( tracer trace.Tracer, context context.T, actionName string, orchestrationDir string, pluginsInfo []contracts.PluginState, output contracts.PluginOutputter) { exectrace := tracer.CurrentTrace() pluginOutputs := inst.execdep.ExecuteDocument(context, pluginsInfo, inst.config.BookKeepingFileName, times.ToIso8601UTC(time.Now()), orchestrationDir) if pluginOutputs == nil { exectrace.WithError(fmt.Errorf("No output from executing %s document", actionName)) output.MarkAsFailed(nil, nil) return } for _, pluginOut := range pluginOutputs { exectrace.WithExitcode(int64(pluginOut.Code)) exectrace.AppendInfof("Plugin %v ResultStatus %v", pluginOut.PluginName, pluginOut.Status) if pluginOut.StandardOutput != "" { exectrace.AppendInfof("%v output: %v", actionName, pluginOut.StandardOutput) } if pluginOut.StandardError != "" { exectrace.AppendErrorf("%v errors: %v", actionName, pluginOut.StandardError) } if pluginOut.Error != "" { exectrace.WithError(errors.New(pluginOut.Error)) output.MarkAsFailed(nil, nil) } output.SetStatus(contracts.MergeResultStatus(output.GetStatus(), pluginOut.Status)) } }