agent/updateutil/updateutil.go (652 lines of code) (raw):
// Copyright 2016 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/
//
// or in the "license" file accompanying this file. This file 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 updateutil contains updater specific utilities.
package updateutil
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"net/url"
"os"
"os/exec"
"path/filepath"
"runtime/debug"
"strconv"
"strings"
"syscall"
"time"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"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/log"
"github.com/aws/amazon-ssm-agent/agent/s3util"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updateconstants"
"github.com/aws/amazon-ssm-agent/agent/updateutil/updateinfo"
"github.com/aws/amazon-ssm-agent/agent/versionutil"
"github.com/aws/amazon-ssm-agent/common/identity"
identity2 "github.com/aws/amazon-ssm-agent/common/identity/identity"
"github.com/aws/amazon-ssm-agent/core/executor"
"github.com/aws/amazon-ssm-agent/core/workerprovider/longrunningprovider/model"
)
type CommandExecutionSettings struct {
Log log.T
Cmd []string
WorkingDir string
UpdaterRoot string
StdOut string
StdErr string
IsAsync bool
Env []string
}
// T represents the interface for Update utility
type T interface {
CreateUpdateDownloadFolder() (folder string, err error)
ExeCommand(input *CommandExecutionSettings) (pid int, exitCode updateconstants.UpdateScriptExitCode, err error)
ExeCommandWithSlice(input *CommandExecutionSettings) (pid int, exitCode updateconstants.UpdateScriptExitCode, err error)
ExecCommandWithOutput(input *CommandExecutionSettings) (pId int, updExitCode updateconstants.UpdateScriptExitCode, stdoutBytes *bytes.Buffer, errorBytes *bytes.Buffer, cmdErr error)
IsServiceRunning(log log.T, i updateinfo.T) (result bool, err error)
IsWorkerRunning(log log.T) (result bool, err error)
WaitForServiceToStart(log log.T, i updateinfo.T, targetVersion string) (result bool, err error)
SaveUpdatePluginResult(log log.T, updaterRoot string, updateResult *UpdatePluginResult) (err error)
IsDiskSpaceSufficientForUpdate(log log.T) (bool, error)
UpdateInstallDelayer(ctx context.T, updateRoot string) error
LoadUpdateDocumentState(ctx context.T, commandId string) error
VerifyInstalledVersion(log log.T, targetVersion string) updateconstants.ErrorCode
UpdateExecutionTimeOut(int)
GetExecutionTimeOut() int
}
// Utility implements interface T
type Utility struct {
Context context.T
CustomUpdateExecutionTimeoutInSeconds int
ProcessExecutor executor.IExecutor
UpdateDocState contracts.DocumentState
}
var getDiskSpaceInfo = fileutil.GetDiskSpaceInfo
var getRemoteProvider = identity2.GetRemoteProvider
var mkDirAll = os.MkdirAll
var openFile = os.OpenFile
var execCommand = exec.Command
var cmdStart = (*exec.Cmd).Start
var cmdOutput = (*exec.Cmd).Output
const (
stateJson = "updatestate.json"
)
func NewUpdaterUtilWithLoadedDocContent(ctx context.T, commandId string) *Utility {
updateUtil := &Utility{
Context: ctx,
}
err := updateUtil.LoadUpdateDocumentState(ctx, commandId)
if err != nil {
ctx.Log().Warnf("Could not load doc content for commandID /%v/: %v", commandId, err)
}
return updateUtil
}
// CreateInstanceInfo create instance related information such as region, platform and arch
func (util *Utility) CreateInstanceInfo(log log.T) (context updateinfo.T, err error) {
return nil, nil
}
// CreateUpdateDownloadFolder creates folder for storing update downloads
func (util *Utility) CreateUpdateDownloadFolder() (folder string, err error) {
root := filepath.Join(appconfig.DownloadRoot, "update")
if err = mkDirAll(root, os.ModePerm|os.ModeDir); err != nil {
return "", err
}
return root, nil
}
// UpdateExecutionTimeOut updates the command execution timeout
func (util *Utility) UpdateExecutionTimeOut(timeout int) {
util.CustomUpdateExecutionTimeoutInSeconds = timeout
}
// GetExecutionTimeOut returns the command execution timeout
func (util *Utility) GetExecutionTimeOut() int {
return util.CustomUpdateExecutionTimeoutInSeconds
}
// ExecCommandWithOutput executes shell command and returns output and error of command execution
func (util *Utility) ExecCommandWithOutput(input *CommandExecutionSettings) (pId int, updExitCode updateconstants.UpdateScriptExitCode, stdoutBytes *bytes.Buffer, errorBytes *bytes.Buffer, cmdErr error) {
log := input.Log
parts := input.Cmd
pid := -1
var updateExitCode updateconstants.UpdateScriptExitCode = -1
tempCmd := setPlatformSpecificCommand(parts)
command := execCommand(tempCmd[0], tempCmd[1:]...)
command.Dir = input.WorkingDir
// Update command env if set by the caller
// If not set, command passes the os.Environ() by default
if input.Env != nil && len(input.Env) > 0 {
command.Env = input.Env
}
stdoutWriter, stderrWriter, err := setExeOutErr(input.UpdaterRoot, input.StdOut, input.StdErr)
if err != nil {
return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, nil, nil, err
}
defer stdoutWriter.Close()
defer stderrWriter.Close()
var errBytes, stdOutBytes bytes.Buffer
command.Stdout = &stdOutBytes
command.Stderr = &errBytes
err = cmdStart(command)
if err != nil {
return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, &stdOutBytes, &errBytes, err
}
pid = GetCommandPid(command)
var timeout = updateconstants.DefaultUpdateExecutionTimeoutInSeconds
if util.CustomUpdateExecutionTimeoutInSeconds != 0 {
timeout = util.CustomUpdateExecutionTimeoutInSeconds
}
timer := time.NewTimer(time.Duration(timeout) * time.Second)
go killProcessOnTimeout(log, command, timer)
err = command.Wait()
if errBytes.Len() != 0 {
stderrWriter.Write(errBytes.Bytes())
}
if stdOutBytes.Len() != 0 {
stdoutWriter.Write(stdOutBytes.Bytes())
}
timedOut := !timer.Stop()
if err != nil {
log.Debugf("command returned error %v", err)
if exitErr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode := status.ExitStatus()
if exitCode == -1 && timedOut {
// set appropriate exit code based on cancel or timeout
exitCode = appconfig.CommandStoppedPreemptivelyExitCode
log.Infof("The execution of command was timedout.")
}
updateExitCode = updateconstants.UpdateScriptExitCode(exitCode)
err = fmt.Errorf("The execution of command returned Exit Status: %d \n %v", exitCode, err.Error())
}
}
return pid, updateExitCode, &stdOutBytes, &errBytes, err
}
return pid, updateExitCode, &stdOutBytes, nil, nil
}
// ExeCommand executes shell command
func (util *Utility) ExeCommand(input *CommandExecutionSettings) (int, updateconstants.UpdateScriptExitCode, error) { // pid, exitCode, error
return util.ExeCommandWithSlice(input)
}
// ExeCommandWithSlice executes shell command
func (util *Utility) ExeCommandWithSlice(input *CommandExecutionSettings) (int, updateconstants.UpdateScriptExitCode, error) { // pid, exitCode, error
log := input.Log
pid := -1
var updateExitCode updateconstants.UpdateScriptExitCode = -1
cmd := input.Cmd
if input.IsAsync {
command := execCommand(cmd[0], cmd[1:]...)
command.Dir = input.WorkingDir
util.setCommandEnvironmentVariables(command, input.Env)
prepareProcess(command)
// Start command asynchronously
err := cmdStart(command)
if err != nil {
return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
}
pid = GetCommandPid(command)
} else {
tempCmd := setPlatformSpecificCommand(cmd)
command := execCommand(tempCmd[0], tempCmd[1:]...)
command.Dir = input.WorkingDir
// Update command env if set by the caller
// If not set, command passes the os.Environ() by default
if input.Env != nil && len(input.Env) > 0 {
command.Env = input.Env
}
stdoutWriter, stderrWriter, err := setExeOutErr(input.UpdaterRoot, input.StdOut, input.StdErr)
if err != nil {
return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
}
defer stdoutWriter.Close()
defer stderrWriter.Close()
command.Stdout = stdoutWriter
command.Stderr = stderrWriter
err = cmdStart(command)
if err != nil {
return pid, updateconstants.ExitCodeErrorPrepareUpdateCommand, err
}
pid = GetCommandPid(command)
var timeout = updateconstants.DefaultUpdateExecutionTimeoutInSeconds
if util.CustomUpdateExecutionTimeoutInSeconds != 0 {
timeout = util.CustomUpdateExecutionTimeoutInSeconds
}
timer := time.NewTimer(time.Duration(timeout) * time.Second)
go killProcessOnTimeout(log, command, timer)
err = command.Wait()
timedOut := !timer.Stop()
if err != nil {
log.Debugf("command returned error %v", err)
if exitErr, ok := err.(*exec.ExitError); ok {
// The program has exited with an exit code != 0
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
exitCode := status.ExitStatus()
if exitCode == -1 && timedOut {
// set appropriate exit code based on cancel or timeout
exitCode = appconfig.CommandStoppedPreemptivelyExitCode
log.Infof("The execution of command was timedout.")
}
updateExitCode = updateconstants.UpdateScriptExitCode(exitCode)
err = fmt.Errorf("The execution of command returned Exit Status: %d \n %v", exitCode, err.Error())
}
}
return pid, updateExitCode, err
}
}
return pid, updateExitCode, nil
}
// TODO move to commandUtil
// ExeCommandOutput executes shell command and returns the stdout
func (util *Utility) ExeCommandOutput(
log log.T,
cmd string,
parameters []string,
workingDir string,
outputRoot string,
stdOutFileName string,
stdErrFileName string,
usePlatformSpecificCommand bool) (output string, err error) {
parts := append([]string{cmd}, parameters...) //strings.Fields(cmd)
var tempCmd []string
if usePlatformSpecificCommand {
tempCmd = setPlatformSpecificCommand(parts)
} else {
tempCmd = parts
}
command := execCommand(tempCmd[0], tempCmd[1:]...)
command.Dir = workingDir
util.setCommandEnvironmentVariables(command, nil)
stdoutWriter, stderrWriter, exeErr := setExeOutErr(outputRoot, stdOutFileName, stdErrFileName)
if exeErr != nil {
return output, exeErr
}
defer stdoutWriter.Close()
defer stderrWriter.Close()
// Don't set command.Stdout - we're going to return it instead of writing it
command.Stderr = stderrWriter
// Run the command and return its output
var out []byte
out, err = cmdOutput(command)
output = string(out)
// Write the returned output so that we can upload it if needed
stdoutWriter.Write(out)
if err != nil {
return
}
return output, err
}
// TODO move to commandUtil
// ExeCommandOutput executes shell command and returns the stdout
func (util *Utility) NewExeCommandOutput(
log log.T,
cmd string,
parameters []string,
workingDir string,
outputRoot string,
stdoutWriter io.Writer,
stderrWriter io.Writer,
usePlatformSpecificCommand bool) (output string, err error) {
parts := append([]string{cmd}, parameters...) //strings.Fields(cmd)
var tempCmd []string
if usePlatformSpecificCommand {
tempCmd = setPlatformSpecificCommand(parts)
} else {
tempCmd = parts
}
command := execCommand(tempCmd[0], tempCmd[1:]...)
command.Dir = workingDir
util.setCommandEnvironmentVariables(command, nil)
// Don't set command.Stdout - we're going to return it instead of writing it
command.Stderr = stderrWriter
// Run the command and return its output
var out []byte
out, err = cmdOutput(command)
output = string(out)
// Write the returned output so that we can upload it if needed
stdoutWriter.Write(out)
if err != nil {
return
}
return output, err
}
// IsServiceRunning returns is service running
func (util *Utility) IsServiceRunning(log log.T, i updateinfo.T) (result bool, err error) {
commandOutput := []byte{}
expectedOutput := ""
isSystemD := false
isDarwin := false
// For mac OS check the running processes
isDarwin = i.IsPlatformDarwin()
var allProcesses []executor.OsProcess
if util.ProcessExecutor == nil {
util.ProcessExecutor = executor.NewProcessExecutor(log)
}
// isSystemD will always be false for Windows
if isSystemD, err = i.IsPlatformUsingSystemD(); err != nil {
return false, err
}
if isSystemD {
expectedOutput = "Active: active (running)"
if commandOutput, err = execCommand("systemctl", "status", "amazon-ssm-agent.service").Output(); err != nil {
//test snap service enabled
if commandOutput, err = execCommand("systemctl", "status", "snap.amazon-ssm-agent.amazon-ssm-agent.service").Output(); err != nil {
return false, err
}
}
agentStatus := strings.TrimSpace(string(commandOutput))
return strings.Contains(agentStatus, expectedOutput), nil
} else if isDarwin {
if allProcesses, err = util.ProcessExecutor.Processes(); err != nil {
return false, err
}
for _, process := range allProcesses {
if process.Executable == updateconstants.DarwinBinaryPath {
return true, nil
}
}
return false, nil
}
return isAgentServiceRunning(log)
}
// IsWorkerRunning returns true if ssm-agent-worker running
func (util *Utility) IsWorkerRunning(log log.T) (result bool, err error) {
var allProcesses []executor.OsProcess
if util.ProcessExecutor == nil {
util.ProcessExecutor = executor.NewProcessExecutor(log)
}
if allProcesses, err = util.ProcessExecutor.Processes(); err != nil {
return false, err
}
for _, process := range allProcesses {
if process.Executable == model.SSMAgentWorkerBinaryName {
log.Infof("Detect SSM Agent worker running")
return true, nil
}
}
// For snap, find the work directory
if _, err := os.Stat(updateconstants.SnapServiceFile); err == nil {
log.Infof("snap is installed")
file, err := os.Open(updateconstants.SnapServiceFile)
if err != nil {
return false, fmt.Errorf("failed to open amazon-ssm-agent.service file %s", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "WorkingDirectory") {
splitResults := strings.Split(line, "/var")
if len(splitResults) <= 1 {
return false, fmt.Errorf("failed to find amazon-ssm-agent working directory %s", line)
}
workerName := filepath.Join(splitResults[1], model.SSMAgentWorkerName)
log.Infof("identified worker name %s", workerName)
for _, process := range allProcesses {
if process.Executable == workerName {
log.Infof("Detect SSM Agent worker running")
return true, nil
}
}
}
}
}
return false, nil
}
// VerifyInstalledVersion verifies whether the expected version is installed on Windows instance.
// For linux, this function always return true
func (util *Utility) VerifyInstalledVersion(log log.T, targetVersion string) updateconstants.ErrorCode {
return verifyVersion(log, targetVersion)
}
// WaitForServiceToStart wait for service to start and returns is service started
func (util *Utility) WaitForServiceToStart(log log.T, i updateinfo.T, targetVersion string) (result bool, svcRunningErr error) {
const (
verifyAttemptCount = 36
verifyRetryIntervalMilliseconds = 5000
)
isRunning := false
isWorkerRunning := true
var workRunningErr error
for attempt := 0; attempt < verifyAttemptCount; attempt++ {
if attempt > 0 {
log.Infof("Retrying update health check %v out of %v", attempt+1, verifyAttemptCount)
time.Sleep(time.Duration(verifyRetryIntervalMilliseconds) * time.Millisecond)
}
isRunning, svcRunningErr = util.IsServiceRunning(log, i)
if isRunning {
log.Infof("health check: amazon-ssm-agent is running")
} else {
log.Infof("health check: amazon-ssm-agent is not running")
}
compareResult, err := versionutil.VersionCompare(targetVersion, updateconstants.SSMAgentWorkerMinVersion)
if err == nil && compareResult >= 0 {
isWorkerRunning, workRunningErr = util.IsWorkerRunning(log)
if isWorkerRunning {
log.Infof("health check: %s is running", model.SSMAgentWorkerName)
} else {
if workRunningErr != nil {
log.Warnf("health check: failed to get state of %s: %v", model.SSMAgentWorkerName, workRunningErr)
} else {
log.Infof("health check: %s is not running", model.SSMAgentWorkerName)
}
}
}
if svcRunningErr == nil && workRunningErr == nil && isRunning && isWorkerRunning {
return true, nil
}
}
errorMessage := "checking svc and agent worker running err"
if svcRunningErr != nil {
errorMessage += ", " + svcRunningErr.Error()
}
if workRunningErr != nil {
errorMessage += ", " + workRunningErr.Error()
}
return false, fmt.Errorf(errorMessage)
}
// IsDiskSpaceSufficientForUpdate loads disk space info and checks the available bytes
// Returns true if the system has at least 100 Mb for available disk space or false if it is less than 100 Mb
func (util *Utility) IsDiskSpaceSufficientForUpdate(log log.T) (bool, error) {
var diskSpaceInfo fileutil.DiskSpaceInfo
var err error
// Get the available disk space
if diskSpaceInfo, err = getDiskSpaceInfo(); err != nil {
log.Infof("Failed to load disk space info - %v", err)
return false, err
}
// Return false if available disk space is less than 100 Mb
if diskSpaceInfo.AvailBytes < updateconstants.MinimumDiskSpaceForUpdate {
log.Infof("Insufficient available disk space - %d Mb", diskSpaceInfo.AvailBytes/int64(1024*1024))
return false, nil
}
// Return true otherwise
return true, nil
}
// BuildMessage builds the messages with provided format, error and arguments
func BuildMessage(err error, format string, params ...interface{}) (message string) {
message = fmt.Sprintf(format, params...)
if err != nil {
message = fmt.Sprintf("%v, ErrorMessage=%v", message, err.Error())
}
return message
}
// BuildMessages builds the messages with provided format, error and arguments
func BuildMessages(errs []error, format string, params ...interface{}) (message string) {
message = fmt.Sprintf(format, params...)
errMessage := ""
if len(errs) > 0 {
for _, err := range errs {
if errMessage == "" {
errMessage = err.Error()
} else {
errMessage = fmt.Sprintf("%v, %v", errMessage, err.Error())
}
}
message = fmt.Sprintf("%v, ErrorMessage=%v", message, errMessage)
}
return message
}
// BuildUpdateCommand builds command string with argument and value
func BuildUpdateCommand(cmd string, arg string, value string) string {
if value == "" || arg == "" {
return cmd
}
return fmt.Sprintf("%v -%v %v", cmd, arg, value)
}
// UpdateArtifactFolder returns the folder path for storing all the update artifacts
func UpdateArtifactFolder(updateRoot string, packageName string, version string) (folder string) {
return filepath.Join(updateRoot, packageName, version)
}
// UpdateContextFilePath returns Context file path
func UpdateContextFilePath(updateRoot string) (filePath string) {
return filepath.Join(updateRoot, updateconstants.UpdateContextFileName)
}
// UpdateOutputDirectory returns output directory
func UpdateOutputDirectory(updateRoot string) string {
return filepath.Join(updateRoot, updateconstants.DefaultOutputFolder)
}
// UpdateStdOutPath returns stand output file path
func UpdateStdOutPath(updateRoot string, fileName string) string {
if fileName == "" {
fileName = updateconstants.DefaultStandOut
}
return filepath.Join(updateRoot, fileName)
}
// UpdateStdErrPath returns stand error file path
func UpdateStdErrPath(updateRoot string, fileName string) string {
if fileName == "" {
fileName = updateconstants.DefaultStandErr
}
return filepath.Join(updateRoot, fileName)
}
// UpdatePluginResultFilePath returns update plugin result file path
func UpdatePluginResultFilePath(updateRoot string) (filePath string) {
return filepath.Join(updateRoot, updateconstants.UpdatePluginResultFileName)
}
// UpdaterFilePath returns updater file path
func UpdaterFilePath(updateRoot string, updaterPackageName string, version string) (filePath string) {
return filepath.Join(UpdateArtifactFolder(updateRoot, updaterPackageName, version), updateconstants.Updater)
}
// InstallerFilePath returns Installer file path
func InstallerFilePath(updateRoot string, packageName string, version string, installer string) (file string) {
return filepath.Join(UpdateArtifactFolder(updateRoot, packageName, version), installer)
}
// UnInstallerFilePath returns UnInstaller file path
func UnInstallerFilePath(updateRoot string, packageName string, version string, uninstaller string) (file string) {
return filepath.Join(UpdateArtifactFolder(updateRoot, packageName, version), uninstaller)
}
func killProcessOnTimeout(log log.T, command *exec.Cmd, timer *time.Timer) {
defer func() {
if r := recover(); r != nil {
log.Errorf("Kill process on timeout panic: \n%v", r)
log.Errorf("Stacktrace:\n%s", debug.Stack())
}
}()
<-timer.C
log.Debug("Process exceeded timeout. Attempting to kill process!")
// task has been exceeded the allowed execution timeout, kill process
if err := command.Process.Kill(); err != nil {
log.Error(err)
return
}
log.Debug("Done kill process!")
}
// setExeOutErr creates stderr and stdout file
func setExeOutErr(
updaterRoot string,
stdOutFileName string,
stdErrFileName string) (stdoutWriter *os.File, stderrWriter *os.File, err error) {
if err = mkDirAll(UpdateOutputDirectory(updaterRoot), appconfig.ReadWriteExecuteAccess); err != nil {
return
}
stdOutPath := UpdateStdOutPath(updaterRoot, stdOutFileName)
stdErrPath := UpdateStdErrPath(updaterRoot, stdErrFileName)
// create stdout file
// Allow append so that if arrays of run command write to the same file, we keep appending to the file.
if stdoutWriter, err = openFile(stdOutPath, appconfig.FileFlagsCreateOrAppend, appconfig.ReadWriteAccess); err != nil {
return
}
// create stderr file
// Allow append so that if arrays of run command write to the same file, we keep appending to the file.
if stderrWriter, err = openFile(stdErrPath, appconfig.FileFlagsCreateOrAppend, appconfig.ReadWriteAccess); err != nil {
return
}
return stdoutWriter, stderrWriter, nil
}
// getCommandPid returns the pid of the process if present, defaults to pid -1
func GetCommandPid(cmd *exec.Cmd) int {
if cmd != nil && cmd.Process != nil {
return cmd.Process.Pid
}
return -1
}
func CompareVersion(versionOne string, versionTwo string) (int, error) {
majorOne, minorOne, buildOne, patchOne, err := parseVersion(versionOne)
if err != nil {
return 0, err
}
majorTwo, minorTwo, buildTwo, patchTwo, err := parseVersion(versionTwo)
if err != nil {
return 0, err
}
if majorOne < majorTwo {
return -1, nil
} else if majorOne > majorTwo {
return 1, nil
}
if minorOne < minorTwo {
return -1, nil
} else if minorOne > minorTwo {
return 1, nil
}
if buildOne < buildTwo {
return -1, nil
} else if buildOne > buildTwo {
return 1, nil
}
if patchOne < patchTwo {
return -1, nil
} else if patchOne > patchTwo {
return 1, nil
}
return 0, nil
}
func parseVersion(version string) (uint64, uint64, uint64, uint64, error) {
parts := strings.SplitN(version, ".", 4)
if len(parts) != 4 {
return 0, 0, 0, 0, errors.New("No Major.Minor.Build.Patch elements found")
}
major, err := strconv.ParseUint(parts[0], 10, 64)
if err != nil {
return 0, 0, 0, 0, err
}
minor, err := strconv.ParseUint(parts[1], 10, 64)
if err != nil {
return 0, 0, 0, 0, err
}
build, err := strconv.ParseUint(parts[2], 10, 64)
if err != nil {
return 0, 0, 0, 0, err
}
patch, err := strconv.ParseUint(parts[3], 10, 64)
if err != nil {
return 0, 0, 0, 0, err
}
return major, minor, build, patch, nil
}
// ConvertToUpdateErrorCode joins the error string array passed and converts to update error code
func ConvertToUpdateErrorCode(errorCodes ...string) updateconstants.ErrorCode {
var updateErrorCode bytes.Buffer
for _, errCode := range errorCodes {
updateErrorCode.WriteString(errCode)
}
return updateconstants.ErrorCode(updateErrorCode.String())
}
// GetManifestURLFromSourceUrl parses source url passed to the updater and generates the url for manifest
func GetManifestURLFromSourceUrl(sourceURL string) (string, error) {
u, err := url.Parse(sourceURL)
if err != nil {
return "", err
}
pathArgs := strings.Split(u.Path, "/")
if len(pathArgs) < 4 || len(pathArgs) > 5 {
return "", fmt.Errorf("URL does not have expected path structure: %s", sourceURL)
} else if len(pathArgs) == 4 {
// Case for: https://{bucket}.s3.{region}.amazonaws.com/amazon-ssm-agent/{version}/amazon-ssm-agent.tar.gz
u.Path = ""
} else {
// Case for: https://s3.{region}.amazonaws.com/{bucket}/amazon-ssm-agent/{version}/amazon-ssm-agent.tar.gz
u.Path = "/" + pathArgs[1]
}
u.Path += "/" + updateconstants.ManifestFile
return u.String(), nil
}
func GetStableURLFromManifestURL(manifestURL string, identity identity.IAgentIdentity) (string, error) {
if !strings.HasSuffix(manifestURL, "/"+updateconstants.ManifestFile) {
return "", fmt.Errorf("unexpected manifest url does not end with manifest file: %s", manifestURL)
}
stableVersionPath := "stable/VERSION"
region, err := identity.Region()
if err != nil {
return "", err
}
stableVersionURL := strings.TrimRight(manifestURL, updateconstants.ManifestFile) + stableVersionPath
stableVersionURL = strings.Replace(stableVersionURL, updateconstants.RegionHolder, region, -1)
return stableVersionURL, nil
}
// ResolveAgentReleaseBucketURL makes best effort to generate an url for the ssm agent bucket
func ResolveAgentReleaseBucketURL(region string, identity identity.IAgentIdentity) string {
s3Url := ""
if dynamicS3Endpoint := identity.GetServiceEndpoint("s3"); dynamicS3Endpoint != "" {
s3Url = "https://" + dynamicS3Endpoint
} else if strings.HasPrefix(region, s3util.ChinaRegionPrefix) {
s3Url = updateconstants.ChinaS3URL
} else {
s3Url = updateconstants.CommonS3URL
}
return strings.Replace(s3Url+updateconstants.BucketPath, updateconstants.RegionHolder, region, -1)
}
// IsV1UpdatePlugin returns true if source agent version is equal or below 3.0.882.0, any error defaults to false
//
// this logic is required since moving logic from plugin to updater would otherwise lead
// to duplicate aws console logging when upgrading from V1UpdatePlugin agents
func IsV1UpdatePlugin(SourceVersion string) bool {
const LastV1UpdatePluginAgentVersion = "3.0.882.0"
comp, err := versionutil.VersionCompare(SourceVersion, LastV1UpdatePluginAgentVersion)
return err == nil && comp <= 0
}
// IsIdentityRuntimeConfigSupported returns true if source agent version is greater than 3.1.282.0, any error defaults to false
// this logic is required to ensure that updater does not rely on an old runtime config if an agent has been downgraded
// from a version that supported identity runtimeconfig and also to skip the runtimeconfig check when we know it does not exist
func IsIdentityRuntimeConfigSupported(sourceVersion string) bool {
const LastAgentVersionBeforeIdentityRuntimeConfig = "3.1.282.0"
comp, err := versionutil.VersionCompare(sourceVersion, LastAgentVersionBeforeIdentityRuntimeConfig)
return err == nil && comp > 0
}
func (util *Utility) setCommandEnvironmentVariables(command *exec.Cmd, commandEnv []string) {
credentialProvider, ok := getRemoteProvider(util.Context.Identity())
if !ok {
return
}
// Don't set environment variables if credentials are not being shared
if !credentialProvider.SharesCredentials() {
return
}
var osEnv []string
// set environment variables
if commandEnv != nil && len(commandEnv) > 0 {
// Update command env if set by the caller
// If not set, command passes the os.Environ() by default
osEnv = commandEnv
} else {
osEnv = os.Environ()
}
command.Env = osEnv
if _, ok := os.LookupEnv("AWS_SHARED_CREDENTIALS_FILE"); !ok && credentialProvider.ShareFile() != "" {
command.Env = append(command.Env, fmt.Sprintf("%s=%s", "AWS_SHARED_CREDENTIALS_FILE", credentialProvider.ShareFile()))
}
if _, ok := os.LookupEnv("AWS_PROFILE"); !ok && credentialProvider.ShareProfile() != "" {
command.Env = append(command.Env, fmt.Sprintf("%s=%s", "AWS_PROFILE", credentialProvider.ShareProfile()))
}
}