agent/setupcli/managers/downloadmanager/downloadmanager.go (252 lines of code) (raw):
// Copyright 2023 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 downloadmanager helps us with file download related functions in ssm-setup-cli
package downloadmanager
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"time"
"github.com/aws/amazon-ssm-agent/agent/appconfig"
"github.com/aws/amazon-ssm-agent/agent/backoffconfig"
"github.com/aws/amazon-ssm-agent/agent/context"
"github.com/aws/amazon-ssm-agent/agent/fileutil"
"github.com/aws/amazon-ssm-agent/agent/log"
"github.com/aws/amazon-ssm-agent/agent/network"
"github.com/aws/amazon-ssm-agent/agent/setupcli/utility"
"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/updateutil/updatemanifest"
"github.com/aws/amazon-ssm-agent/common/identity/endpoint"
"github.com/cenkalti/backoff/v4"
)
const (
manifestJsonFileName = "ssm-agent-manifest.json"
s3Service = "s3"
lowerKernelVersionSupportedAgent = "3.0.1479.0"
testVersion = "255.255.65535.999"
)
var (
utilHttpDownload = utility.HttpDownload
updateInfoNew = updateinfo.New
updateManifestNew = updatemanifest.New
fileUtilUnCompress = fileutil.Uncompress
fileUtilityReadContent = utility.HttpReadContent
backOffRetry = backoff.Retry
computeAgentChecksumFunc = utility.ComputeCheckSum
hasLowerKernelVersionFunc = hasLowerKernelVersion
)
type downloadManager struct {
log log.T
ctx context.T
bucketUrl string
region string
version string
manifestURL string
updateInfo updateinfo.T
manifestInfo updatemanifest.T
isNano bool
artifactsPath string
}
// New returns a new instance of DownloadManager
func New(log log.T, region string, manifestURL string, updateInfo updateinfo.T, setupCLIArtifactsPath string, isNano bool) IDownloadManager {
downloadMgrLog := log.WithContext("[DownloadManager]")
var err error
ctx := context.Default(downloadMgrLog, appconfig.DefaultConfig(), nil)
if updateInfo == nil {
updateInfo, err = updateinfo.New(ctx)
if err != nil {
downloadMgrLog.Errorf("Error while initiating update Info: %v", err)
return nil
}
}
endpointHelper := endpoint.NewEndpointHelper(log, appconfig.SsmagentConfig{})
if endpointHelper == nil {
downloadMgrLog.Errorf("Error while initiating endpoint helper: %v", err)
return nil
}
s3Endpoint := endpointHelper.GetServiceEndpoint(s3Service, region)
downloadManagerRef := &downloadManager{
log: downloadMgrLog,
ctx: ctx,
region: region,
updateInfo: updateInfo,
bucketUrl: s3Endpoint,
manifestURL: manifestURL, // field is optional
isNano: isNano,
artifactsPath: setupCLIArtifactsPath,
}
err = downloadManagerRef.Init()
if err != nil {
downloadMgrLog.Errorf("initialization failed: %v", err)
return nil
}
return downloadManagerRef
}
func (d *downloadManager) Init() error {
s3Url := d.getRegionManifestUrl()
// downloads manifest based on the URL retrieved above and stores it in local path
manifestFilePath, err := utilHttpDownload(d.log, s3Url, d.artifactsPath)
if err != nil || manifestFilePath == "" {
return fmt.Errorf("error while downloading manifest: %v", err)
}
updateManifestObj := updateManifestNew(d.ctx, d.updateInfo, d.region)
d.manifestInfo = updateManifestObj
err = updateManifestObj.LoadManifest(manifestFilePath)
if err != nil {
return fmt.Errorf("error while loading manifest: %v", err)
}
return nil
}
// DownloadArtifacts downloads agent artifacts from S3 bucket
func (d *downloadManager) DownloadArtifacts(installVersion string, manifestUrl string, artifactsStorePath string) error {
var agentDownloadURL, agentHashInManifest string
logger := d.log
var err error
// generate agent artifacts URL and checksum using the manifest loaded
if agentDownloadURL, agentHashInManifest, err = d.manifestInfo.GetDownloadURLAndHash(appconfig.DefaultAgentName, installVersion); err != nil {
return fmt.Errorf("error while getting target location and target hash: %v", err)
}
generatedUrl := d.getS3BucketUrl() + "/"
generatedUrl += appconfig.DefaultAgentName + "/" + installVersion + "/" + d.updateInfo.GenerateCompressedFileName(appconfig.DefaultAgentName)
if generatedUrl != agentDownloadURL {
d.log.Warnf("URL does not match %v %v", generatedUrl, agentDownloadURL)
}
// download agent artifacts using the generated URL before and store in local path
agentSetupFilePath, err := utilHttpDownload(logger, generatedUrl, artifactsStorePath)
if err != nil || agentSetupFilePath == "" {
return fmt.Errorf("error while downloading agent artifacts file: %v", err)
}
// compute checksum of downloaded binary
agentCheckSum, err := computeAgentChecksumFunc(agentSetupFilePath)
if err != nil {
return fmt.Errorf("failed to fetch checksum: %v", err)
}
// validate checksum using manifest
if agentCheckSum != agentHashInManifest {
return fmt.Errorf("checksum validation failed: %v", err)
}
// Un-compress downloaded files
err = d.fileUnCompress(logger, agentSetupFilePath, artifactsStorePath)
return err
}
// DownloadLatestSSMSetupCLI downloads latest SSM Setup CLI
func (d *downloadManager) DownloadLatestSSMSetupCLI(artifactsStorePath string, expectedSetupCLICheckSum string) error {
logger := d.log
logger.Info("Downloading SSM Setup CLI")
folderName := d.updateInfo.GeneratePlatformBasedFolderName()
// generate ssm-setup-cli s3 url
ssmSetupCLIS3URL, err := d.generateLatestSSMSetupCLIS3Url(folderName)
if err != nil {
return fmt.Errorf("error while generating SSM Setup CLI URL: %v", err)
}
// Download ssm-setup CLI
downloadedSSMSetupCLIFilePath, err := utilHttpDownload(logger, ssmSetupCLIS3URL, artifactsStorePath)
if err != nil || downloadedSSMSetupCLIFilePath == "" {
return fmt.Errorf("error while downloading SSM Setup CLI: %v", err)
}
// compute checksum of downloaded binary
downloadedCLICheckSum, err := computeAgentChecksumFunc(downloadedSSMSetupCLIFilePath)
if err != nil {
return fmt.Errorf("failed to fetch checksum: %v", err)
}
if downloadedCLICheckSum != expectedSetupCLICheckSum {
return fmt.Errorf("checksum mismatch with latest ssm-setup-cli. Please retry after downloading latest ssm-setup-cli")
}
logger.Infof("Downloaded SSM-Setup-CLI successfully")
return nil
}
// GetStableVersion downloads the stable version file and returns the stable version number
func (d *downloadManager) GetStableVersion() (string, error) {
stableVersionURL, err := d.getStableVersionURL()
if err != nil {
return "", fmt.Errorf("error while generating stable version URL %v", err)
}
stableVersion, err := d.readVersionFromURL(stableVersionURL)
return stableVersion, err
}
// GetLatestVersion downloads the latest version file and returns the latest version number
func (d *downloadManager) GetLatestVersion() (string, error) {
if hasLowerKernelVersionFunc() {
return lowerKernelVersionSupportedAgent, nil
}
latestVersion, err := d.manifestInfo.GetLatestActiveVersion(appconfig.DefaultAgentName)
if err != nil {
return "", fmt.Errorf("error while getting the latest version from manifest: %v", err)
}
if latestVersion == testVersion {
latestVersionURL, err := d.getLatestVersionURL()
if err != nil {
return "", fmt.Errorf("error while generating latest version URL %v", err)
}
latestVersion, err := d.readVersionFromURL(latestVersionURL)
return latestVersion, err
}
return latestVersion, err
}
// getLatestVersionURL gets the latest version from URL
func (d *downloadManager) getLatestVersionURL() (string, error) {
latestVersionURL := fmt.Sprintf("%s/%s/%s", d.getS3BucketUrl(), utility.LatestVersionString, utility.VersionFile)
s3URL, err := url.Parse(latestVersionURL)
if err != nil {
return "", fmt.Errorf("error while parsing s3URL: %v", err)
}
return s3URL.String(), nil
}
func (d *downloadManager) readVersionFromURL(versionURL string) (string, error) {
var err error
d.log.Infof("Retrieving version from: %s", versionURL)
exponentialBackOff, err := backoffconfig.GetDefaultExponentialBackoff()
if err != nil {
return "", fmt.Errorf("failed to initialize backoff module: %v", err)
}
var content string
err = backOffRetry(func() error {
httpTimeout := 30 * time.Second
tr := network.GetDefaultTransport(d.log, appconfig.DefaultConfig())
client := &http.Client{
Transport: tr,
Timeout: httpTimeout,
}
// use http client to download
contentBytes, readErr := fileUtilityReadContent(versionURL, client)
if readErr != nil {
return fmt.Errorf("failed to read response from %s: %v", versionURL, readErr)
}
if contentBytes == nil {
return fmt.Errorf("response code is nil")
}
content = string(contentBytes)
return nil
}, exponentialBackOff)
if err != nil {
return "", fmt.Errorf("failed to get version from %s: %v", versionURL, err)
}
version := strings.TrimSpace(content)
if !regexp.MustCompile(`^\d+.\d+.\d+.\d+$`).Match([]byte(version)) {
return "", fmt.Errorf("invalid version format returned from %s: %s", versionURL, version)
}
d.log.Infof("Got version from %v: %s", versionURL, version)
return version, nil
}
func (d *downloadManager) generateLatestSSMSetupCLIS3Url(folderName string) (string, error) {
s3BucketUrl := d.getS3BucketUrl()
ssmSetupCLIURL := fmt.Sprintf("%s/%s/%s/%s", s3BucketUrl, utility.LatestVersionString, folderName, utility.SSMSetupCLIBinary)
s3URL, err := url.Parse(ssmSetupCLIURL)
if err != nil {
return "", fmt.Errorf("error while parsing s3URL")
}
return s3URL.String(), nil
}
// getS3BucketUrl returns s3 bucket URL
func (d *downloadManager) getS3BucketUrl() string {
httpsPrefix := "https://"
if d.manifestURL != "" {
url := strings.TrimSpace(d.manifestURL)
url = strings.TrimRight(url, updateconstants.ManifestFile)
url = strings.TrimSuffix(url, "/")
url = strings.TrimSuffix(url, "\\")
return url
}
bucketName := strings.TrimSuffix(updateconstants.BucketPath, "/")
return strings.Replace(httpsPrefix+d.bucketUrl+bucketName, updateconstants.RegionHolder, d.region, -1)
}
func (d *downloadManager) getStableVersionURL() (string, error) {
s3BucketUrl := d.getS3BucketUrl()
stableVersionURL := fmt.Sprintf("%s/%s/%s", s3BucketUrl, utility.StableVersionString, utility.VersionFile)
s3URL, err := url.Parse(stableVersionURL)
if err != nil {
return "", fmt.Errorf("error while parsing stable version S3 URL: %v", err)
}
return s3URL.String(), nil
}
// getRegionManifestUrl gets region based manifest URL
func (d *downloadManager) getRegionManifestUrl() string {
s3BucketUrl := d.getS3BucketUrl()
s3URL, err := url.Parse(fmt.Sprintf("%s"+"/"+manifestJsonFileName, s3BucketUrl))
if err != nil {
return ""
}
return s3URL.String()
}