// Copyright 2018 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 documentarchive contains the struct that is called when the package information is stored in birdwatcher package documentarchive import ( "crypto/sha256" "errors" "fmt" "math/rand" "strings" "time" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/archive" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/birdwatcher/facade" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/packageservice" "github.com/aws/amazon-ssm-agent/agent/plugins/configurepackage/trace" "github.com/aws/aws-sdk-go/service/ssm" ) const ( // birdwatcher packages support `latest` as a version latestVersion = "latest" ) type PackageArchive struct { facadeClient facade.BirdwatcherFacade manifestCache packageservice.ManifestCache archiveType string documentDesc *ssm.DocumentDescription documentArn string manifest string docVersion string attachments []*ssm.AttachmentContent timeUnit int } // constructors // New is a constructor for PackageArchive struct func New(facadeClientSession facade.BirdwatcherFacade) archive.IPackageArchive { return &PackageArchive{ facadeClient: facadeClientSession, archiveType: archive.PackageArchiveDocument, timeUnit: 1000, //Adding here to be able to change for testing, 1000 millisecond to make a second } } // NewDocumentArchive is a constructor for PackageArchive struct (meant for testing) func NewDocumentArchive(facadeClientSession facade.BirdwatcherFacade, attachmentsContent []*ssm.AttachmentContent, docDescription *ssm.DocumentDescription, cache packageservice.ManifestCache, manifestStr string) archive.IPackageArchive { return &PackageArchive{ facadeClient: facadeClientSession, archiveType: archive.PackageArchiveDocument, attachments: attachmentsContent, documentDesc: docDescription, docVersion: *docDescription.DocumentVersion, manifestCache: cache, manifest: manifestStr, documentArn: *docDescription.Name, timeUnit: 0, //No sleep for testing } } // Name of archive type func (da *PackageArchive) Name() string { return da.archiveType } // setters // SetPackageName sets the document arn. The manifest and version is not required // since we use the document name and document version func (da *PackageArchive) SetResource(packageName string, version string, manifest *birdwatcher.Manifest) { if da.documentDesc != nil { da.documentArn = *da.documentDesc.Name da.docVersion = *da.documentDesc.DocumentVersion } } // SetManifestCache sets the manifest Cache func (da *PackageArchive) SetManifestCache(manifestCache packageservice.ManifestCache) { da.manifestCache = manifestCache } // getters // GetResourceVersion makes a call to birdwatcher API to figure the right version of the resource that needs to be installed func (da *PackageArchive) GetResourceVersion(packageName string, packageVersion string) (name string, version string) { // Return the packageVersion as "" if empty and return version if specified. return packageName, packageVersion } // GetResourceArn returns the document Arn required for storing the file. This is found in the response of GetDocument. func (da *PackageArchive) GetResourceArn(packageName string, version string) string { // GetDocument returns the Name of the document if it belongs to the account of the instance. // If it is a shared document, GetDocument returns the document ARN as Name return da.documentArn } // GetFileDownloadLocation obtains the location of the file in the archive // in the document archive, this information is stored in the attachmentContent // field in the reult of GetDocument. func (da *PackageArchive) GetFileDownloadLocation(file *archive.File, packageName string, version string) (string, error) { if file == nil { return "", errors.New("Could not obtain the file from manifest") } fileName := file.Name if da.attachments == nil { if _, err := da.getDocument(packageName, version); err != nil { return "", err } } // if the attachments are still nil, return error if da.attachments == nil { return "", fmt.Errorf("Could not retreive the attachments for installation") } for _, attachmentContent := range da.attachments { if *attachmentContent.Name == fileName { return *attachmentContent.Url, nil } } return "", fmt.Errorf("Install attachments for package does not exist") } // DownloadArtifactInfo downloads the document using GetDocument and eventually gets the manifest from that and returns it func (da *PackageArchive) DownloadArchiveInfo(tracer trace.Tracer, packageName string, version string) (string, error) { var cachedDocumentHash string var err error // SSM Documents don't support `latest` as version, convert it to empty if strings.EqualFold(version, latestVersion) { version = "" } versionPtr := &version if version == "" { versionPtr = nil } MaxDelayBeforeCall := 15 //seconds // random back off before GetDocument call time.Sleep(time.Duration(getRandomBackOffTime(MaxDelayBeforeCall)*(da.timeUnit)) * time.Millisecond) descDocResponse, err := da.facadeClient.DescribeDocument( &ssm.DescribeDocumentInput{ Name: &packageName, VersionName: versionPtr, }, ) if err != nil { return "", fmt.Errorf("failed to retrieve package document description: %v", err) } if descDocResponse == nil || descDocResponse.Document == nil { return "", fmt.Errorf("Failed to retrieve document description for package installation") } if *descDocResponse.Document.Status != ssm.DocumentStatusActive { return "", fmt.Errorf("package document is not currently active, kindly retry when the status is active. Current document status - %v", *descDocResponse.Document.Status) } if descDocResponse.Document.Name == nil || *descDocResponse.Document.Name == "" { return "", fmt.Errorf("document name cannot be empty") } if descDocResponse.Document.DocumentVersion == nil { return "", fmt.Errorf("document version cannot be nil") } da.documentDesc = descDocResponse.Document // DescribeDocument returns the Name of the document if it belongs to the account of the instance. // If it is a shared document, DescribeDocument returns the document ARN as Name da.documentArn = *da.documentDesc.Name da.docVersion = *da.documentDesc.DocumentVersion // Read the cached manifest hash file hashData, err := da.manifestCache.ReadManifestHash(packageName, da.docVersion) if err != nil { // if manifest hash does not exist, getDocument to download the manifest return da.getDocument(packageName, version) } // if no error, proceed to describe document cachedDocumentHash = string(hashData) if cachedDocumentHash != *da.documentDesc.Hash { // If the hash value of the cached hash and the document hash isn't the same, then getDocument to download the content return da.getDocument(packageName, version) } else { // If the hash matches, read the cached manifest. If there is an error reading the manifest, because // it does not exist or is corrupted, getDocument to download the manifest manifestData, err := da.manifestCache.ReadManifest(*da.documentDesc.Name, da.docVersion) if err != nil || string(manifestData) == "" { return da.getDocument(packageName, version) } // if hash of stored anifest is the same // Store manifests separately for document type packages da.manifest = string(manifestData) } return da.manifest, nil } // ReadManifestFromCache reads the manifest that was stored in manifestCache, if present // Document packages store the manifest with the document version func (da *PackageArchive) ReadManifestFromCache(packageArn string, version string) (*birdwatcher.Manifest, error) { data, err := da.manifestCache.ReadManifest(da.documentArn, da.docVersion) if err != nil { return nil, err } return archive.ParseManifest(&data) } // WriteManifestToCache stores the manifest in manifestCache func (da *PackageArchive) WriteManifestToCache(packageArn string, version string, manifest []byte) error { return da.manifestCache.WriteManifest(da.documentArn, da.docVersion, manifest) } func (da *PackageArchive) DeleteCachedManifest(packageArn string, version string) error { return da.manifestCache.DeleteManifest(da.documentArn, da.docVersion) } // helpers func (da *PackageArchive) generateAndSaveManifestHash(name, manifest, documentVersion string) error { var hashChannel = make(chan []byte, 1) hash := sha256.Sum256([]byte(manifest)) hashChannel <- hash[:] return da.manifestCache.WriteManifestHash(name, documentVersion, <-hashChannel) } func (da *PackageArchive) getDocument(packageName, version string) (manifest string, err error) { versionPtr := &version if version == "" { versionPtr = nil } getDocResponse, err := da.facadeClient.GetDocument( &ssm.GetDocumentInput{ Name: &packageName, VersionName: versionPtr, }, ) if err != nil { return "", fmt.Errorf("failed to retrieve package document: %v", err) } if getDocResponse == nil { return "", fmt.Errorf("Failed to retreive document for package installation") } if *getDocResponse.Status != ssm.DocumentStatusActive { return "", fmt.Errorf("package document is not currently active, kindly retry when the status is active. Current document status - %v", *getDocResponse.Status) } // GetDocument returns the Name of the document if it belongs to the account of the instance. // If it is a shared document, GetDocument returns the document ARN as Name if getDocResponse.Name == nil || *getDocResponse.Name == "" { return "", fmt.Errorf("document name cannot be empty") } da.documentArn = *getDocResponse.Name if getDocResponse.Content == nil || *getDocResponse.Content == "" { return "", fmt.Errorf("failed to retrieve manifest from package document") } if getDocResponse.DocumentVersion == nil { return "", fmt.Errorf("document version cannot be nil") } //TODO: add check for docVersion da.manifest = *getDocResponse.Content da.attachments = getDocResponse.AttachmentsContent da.docVersion = *getDocResponse.DocumentVersion // Set the document description to nil since it doesn't apply anymore da.documentDesc = nil err = da.generateAndSaveManifestHash(packageName, da.manifest, da.docVersion) return da.manifest, err } func getRandomBackOffTime(timeInSeconds int) int { rand.Seed(time.Now().UnixNano()) delay := rand.Intn(timeInSeconds) + 1 return delay }