ecs-agent/api/attachment/resource/resource_attachment.go (247 lines of code) (raw):
// Copyright 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 resource
import (
"errors"
"fmt"
"sync"
"time"
"github.com/aws/amazon-ecs-agent/ecs-agent/api/attachment"
"github.com/aws/amazon-ecs-agent/ecs-agent/logger"
"github.com/aws/amazon-ecs-agent/ecs-agent/utils/ttime"
)
type ResourceAttachment struct {
attachment.AttachmentInfo
// AttachmentType is the type of the resource attachment which can be "amazonebs" for EBS attach tasks.
AttachmentType string `json:"AttachmentType,omitempty"`
// AttachmentProperties is a map storing (name, value) representation of attachment properties.
// Each pair is a set of property of one resource attachment.
// The "FargateResourceId" is a property name that will be present for all resources.
// Other properties can vary based on the resource.
// For example, if the attachment is used for an EBS volume resource, the additional properties will be
// the customer specified volume size, and the image cache size.
AttachmentProperties map[string]string `json:"AttachmentProperties,omitempty"`
// ackTimer is used to register the expiration timeout callback for unsuccessful
// Resource attachments
ackTimer ttime.Timer
// guard protects access to fields of this struct
guard sync.RWMutex
err error
}
// Agent Communication Service (ACS) can send messages of type ConfirmAttachmentMessage. These messages include
// an attachment, and map of associated properties. The below list contains attachment properties which Agent can use
// to validate various types of attachments.
const (
// Common properties.
ResourceTypeName = "resourceType"
// Properties specific to volumes.
VolumeIdName = "volumeID"
DeviceName = "deviceName" // name of the block device on the instance where the volume is attached
// Properties specific to resources provisioned by Fargate Control Plane.
FargateResourceIdName = "resourceID"
// Properties specific to Extensible Ephemeral Storage (EES).
VolumeSizeInGiBName = "volumeSizeInGiB" // the total size of the EES (requested size + image cache size)
RequestedSizeName = "requestedSizeInGiB" // the customer requested size of extensible ephemeral storage
// Properties specific to Elastic Block Service Volumes
FileSystemTypeName = "filesystemType"
// Properties specific to volumes for EBS attach.
VolumeIdKey = "volumeId"
VolumeSizeGibKey = "volumeSizeGib"
DeviceNameKey = "deviceName"
SourceVolumeHostPathKey = "sourceVolumeHostPath"
VolumeNameKey = "volumeName"
FileSystemKey = "fileSystem"
)
var (
allowedFSTypes = map[string]bool{
"xfs": true,
"ext2": true,
"ext3": true,
"ext4": true,
"ntfs": true,
}
)
// getCommonProperties returns the common properties as used for validating a resource.
func getCommonProperties() (commonProperties []string) {
commonProperties = []string{
ResourceTypeName,
}
return commonProperties
}
// getVolumeSpecificProperties returns the properties specific to volume resources.
func getVolumeSpecificProperties() (volumeSpecificProperties []string) {
volumeSpecificProperties = []string{
VolumeIdName,
DeviceName,
}
return volumeSpecificProperties
}
// GetVolumeSpecificPropertiesForEBSAttach returns the properties specific to EBS volume resources which will be used
// in EBS attach.
func GetVolumeSpecificPropertiesForEBSAttach() (volumeSpecificProperties []string) {
volumeSpecificProperties = []string{
VolumeIdKey,
VolumeSizeGibKey,
DeviceNameKey,
SourceVolumeHostPathKey,
VolumeNameKey,
}
return volumeSpecificProperties
}
// getFargateControlPlaneProperties returns the properties specific to resources provisioned by Fargate control plane.
func getFargateControlPlaneProperties() (fargateCpProperties []string) {
fargateCpProperties = []string{
FargateResourceIdName,
}
return fargateCpProperties
}
// getExtensibleEphemeralStorageProperties returns the properties specific to extensible ephemeral storage resources.
func getExtensibleEphemeralStorageProperties() (ephemeralStorageProperties []string) {
ephemeralStorageProperties = []string{
VolumeSizeInGiBName,
RequestedSizeName,
}
ephemeralStorageProperties = append(ephemeralStorageProperties, getFargateControlPlaneProperties()...)
return ephemeralStorageProperties
}
func getResourceAttachmentLogFields(ra *ResourceAttachment, duration time.Duration) logger.Fields {
fields := logger.Fields{
"duration": duration.String(),
"attachmentARN": ra.AttachmentARN,
"attachmentType": ra.AttachmentType,
"attachmentSent": ra.AttachStatusSent,
"volumeName": ra.AttachmentProperties[VolumeNameKey],
"volumeSizeInGib": ra.AttachmentProperties[VolumeSizeGibKey],
"sourceVolumeHostPath": ra.AttachmentProperties[SourceVolumeHostPathKey],
"volumeId": ra.AttachmentProperties[VolumeIdKey],
"deviceName": ra.AttachmentProperties[DeviceNameKey],
"fileSystem": ra.AttachmentProperties[FileSystemKey],
"status": ra.Status.String(),
"expiresAt": ra.ExpiresAt.Format(time.RFC3339),
}
return fields
}
// StartTimer starts the ack timer to record the expiration of resource attachment
func (ra *ResourceAttachment) StartTimer(timeoutFunc func()) error {
ra.guard.Lock()
defer ra.guard.Unlock()
if ra.ackTimer != nil {
// The timer has already been initialized, do nothing
return nil
}
now := time.Now()
duration := ra.ExpiresAt.Sub(now)
if duration <= 0 {
return fmt.Errorf("resource attachment: timer expiration is in the past; expiration [%s] < now [%s]",
ra.ExpiresAt.String(), now.String())
}
logger.Info("Starting resource attachment ack timer", getResourceAttachmentLogFields(ra, duration))
ra.ackTimer = time.AfterFunc(duration, timeoutFunc)
return nil
}
// Initialize initializes the fields that can't be populated from loading state file.
// Notably, this initializes the ack timer so that if we time out waiting for the resource to be attached, the attachment
// can be removed from state.
func (ra *ResourceAttachment) Initialize(timeoutFunc func()) error {
ra.guard.Lock()
defer ra.guard.Unlock()
if ra.AttachStatusSent { // resource attachment status has been sent, no need to start ack timer.
return nil
}
now := time.Now()
duration := ra.ExpiresAt.Sub(now)
if duration <= 0 {
return errors.New("resource attachment has already expired")
}
logger.Info("Starting Resource attachment ack timer", getResourceAttachmentLogFields(ra, duration))
ra.ackTimer = time.AfterFunc(duration, timeoutFunc)
return nil
}
// IsSent checks if the resource attachment attached status has been sent
func (ra *ResourceAttachment) IsSent() bool {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.AttachStatusSent
}
// SetSentStatus marks the resource attachment attached status has been sent
func (ra *ResourceAttachment) SetSentStatus() {
ra.guard.Lock()
defer ra.guard.Unlock()
ra.AttachStatusSent = true
}
// IsAttached checks if the resource attachment has been found attached on the host
func (ra *ResourceAttachment) IsAttached() bool {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.Status == attachment.AttachmentAttached
}
// SetAttachedStatus marks the resouce attachment as attached once it's been found on the host
func (ra *ResourceAttachment) SetAttachedStatus() {
ra.guard.Lock()
defer ra.guard.Unlock()
ra.Status = attachment.AttachmentAttached
}
// StopAckTimer stops the ack timer set on the resource attachment
func (ra *ResourceAttachment) StopAckTimer() {
ra.guard.Lock()
defer ra.guard.Unlock()
ra.ackTimer.Stop()
}
// HasExpired returns true if the resource attachment object has exceeded the
// threshold for notifying the backend of the attachment
func (ra *ResourceAttachment) HasExpired() bool {
ra.guard.RLock()
defer ra.guard.RUnlock()
return time.Now().After(ra.ExpiresAt)
}
// SetError sets the error for a resource attachment if it can't be found.
func (ra *ResourceAttachment) SetError(err error) {
ra.guard.Lock()
defer ra.guard.Unlock()
ra.err = err
}
// GetError returns the error field for a resource attachment.
func (ra *ResourceAttachment) GetError() error {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.err
}
// EBSToString returns a string representation of an EBS volume resource attachment.
func (ra *ResourceAttachment) EBSToString() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.ebsToStringUnsafe()
}
func (ra *ResourceAttachment) ebsToStringUnsafe() string {
return fmt.Sprintf(
"Resource Attachment: arn=%s attachmentType=%s attachmentSent=%t volumeName=%s fileSystem=%s volumeId=%s volumeSizeInGib=%s deviceName=%s sourceVolumeHostPath=%s status=%s expiresAt=%s error=%v",
ra.AttachmentARN, ra.AttachmentType, ra.AttachStatusSent, ra.AttachmentProperties[VolumeNameKey], ra.AttachmentProperties[FileSystemKey], ra.AttachmentProperties[VolumeIdKey], ra.AttachmentProperties[VolumeSizeGibKey],
ra.AttachmentProperties[DeviceNameKey], ra.AttachmentProperties[SourceVolumeHostPathKey], ra.Status.String(), ra.ExpiresAt.Format(time.RFC3339), ra.err)
}
// GetAttachmentProperties returns the specific attachment property of the resource attachment object
func (ra *ResourceAttachment) GetAttachmentProperties(key string) string {
ra.guard.RLock()
defer ra.guard.RUnlock()
val, ok := ra.AttachmentProperties[key]
if ok {
return val
}
return ""
}
func (ra *ResourceAttachment) GetAttachmentType() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.AttachmentType
}
func (ra *ResourceAttachment) SetDeviceName(deviceName string) {
ra.guard.Lock()
defer ra.guard.Unlock()
ra.AttachmentProperties[DeviceNameKey] = deviceName
}
func (ra *ResourceAttachment) GetAttachmentARN() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.AttachmentARN
}
func (ra *ResourceAttachment) GetStatus() attachment.AttachmentStatus {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.Status
}
func (ra *ResourceAttachment) GetExpiresAt() time.Time {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.ExpiresAt
}
func (ra *ResourceAttachment) GetClusterARN() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.ClusterARN
}
func (ra *ResourceAttachment) GetContainerInstanceARN() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.ContainerInstanceARN
}
// should attach when not attached, and not sent/not expired
func (ra *ResourceAttachment) ShouldAttach() bool {
ra.guard.RLock()
defer ra.guard.RUnlock()
return !(ra.Status == attachment.AttachmentAttached) && !ra.AttachStatusSent && !(time.Now().After(ra.ExpiresAt))
}
// should notify when attached, and not sent/not expired
func (ra *ResourceAttachment) ShouldNotify() bool {
ra.guard.RLock()
defer ra.guard.RUnlock()
return (ra.Status == attachment.AttachmentAttached) && !ra.AttachStatusSent && !(time.Now().After(ra.ExpiresAt))
}
func (ra *ResourceAttachment) String() string {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.stringUnsafe()
}
func (ra *ResourceAttachment) GetAttachmentStatus() attachment.AttachmentStatus {
ra.guard.RLock()
defer ra.guard.RUnlock()
return ra.Status
}
// stringUnsafe returns a string representation of the ENI Attachment
func (ra *ResourceAttachment) stringUnsafe() string {
return fmt.Sprintf(
"Resource Attachment: attachment=%s attachmentType=%s attachmentSent=%t status=%s expiresAt=%s",
ra.AttachmentARN, ra.AttachmentType, ra.AttachStatusSent, ra.Status.String(), ra.ExpiresAt.Format(time.RFC3339))
}