agent/plugins/configurepackage/ssminstaller/ssminstaller.go (260 lines of code) (raw):
// 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))
}
}