From 11ad1e06ae29130df225f080b4c1a3529ca3c02f Mon Sep 17 00:00:00 2001 From: Dane H Lim Date: Mon, 17 Jul 2023 12:05:43 -0700 Subject: [PATCH] Add ACS attach resource responder to ecs-agent --- .../acs/session/attach_resource_responder.go | 189 ++++++++++++++++++ .../api/resource/resource_attachment.go | 85 ++++++++ .../ecs-agent/api/resource/resource_type.go | 19 ++ .../api/resource/resource_validation.go | 78 ++++++++ .../ecs-agent/metrics/constants.go | 4 + agent/vendor/modules.txt | 1 + .../acs/session/attach_resource_responder.go | 189 ++++++++++++++++++ .../session/attach_resource_responder_test.go | 147 ++++++++++++++ ecs-agent/api/resource/resource_attachment.go | 85 ++++++++ ecs-agent/api/resource/resource_type.go | 19 ++ ecs-agent/api/resource/resource_validation.go | 78 ++++++++ .../api/resource/resource_validation_test.go | 85 ++++++++ ecs-agent/metrics/constants.go | 4 + 13 files changed, 983 insertions(+) create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/acs/session/attach_resource_responder.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_attachment.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_type.go create mode 100644 agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_validation.go create mode 100644 ecs-agent/acs/session/attach_resource_responder.go create mode 100644 ecs-agent/acs/session/attach_resource_responder_test.go create mode 100644 ecs-agent/api/resource/resource_attachment.go create mode 100644 ecs-agent/api/resource/resource_type.go create mode 100644 ecs-agent/api/resource/resource_validation.go create mode 100644 ecs-agent/api/resource/resource_validation_test.go diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/acs/session/attach_resource_responder.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/acs/session/attach_resource_responder.go new file mode 100644 index 0000000000..4ad3ba64c3 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/acs/session/attach_resource_responder.go @@ -0,0 +1,189 @@ +// 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 session + +import ( + "fmt" + "time" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/attachmentinfo" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/resource" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/status" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" + "github.com/aws/amazon-ecs-agent/ecs-agent/metrics" + "github.com/aws/amazon-ecs-agent/ecs-agent/wsclient" + + "github.com/aws/aws-sdk-go/aws" + awsARN "github.com/aws/aws-sdk-go/aws/arn" + "github.com/pkg/errors" +) + +const ( + AttachResourceMessageName = "ConfirmAttachmentMessage" +) + +type ResourceHandler interface { + HandleResourceAttachment(Attachment *resource.ResourceAttachment) +} + +// attachResourceResponder implements the wsclient.RequestResponder interface for responding +// to ecsacs.ConfirmAttachmentMessage messages sent by ACS. +type attachResourceResponder struct { + resourceHandler ResourceHandler + metricsFactory metrics.EntryFactory + respond wsclient.RespondFunc +} + +func NewAttachResourceResponder(resourceHandler ResourceHandler, metricsFactory metrics.EntryFactory, + responseSender wsclient.RespondFunc) wsclient.RequestResponder { + r := &attachResourceResponder{ + resourceHandler: resourceHandler, + metricsFactory: metricsFactory, + } + r.respond = ResponseToACSSender(r.Name(), responseSender) + return r +} + +func (*attachResourceResponder) Name() string { return "attach resource responder" } + +func (r *attachResourceResponder) HandlerFunc() wsclient.RequestHandler { + return r.handleAttachMessage +} + +func (r *attachResourceResponder) handleAttachMessage(message *ecsacs.ConfirmAttachmentMessage) { + logger.Debug(fmt.Sprintf("Handling %s", AttachResourceMessageName)) + receivedAt := time.Now() + + // Validate fields in the message. + attachmentProperties, err := validateAttachResourceMessage(message) + r.metricsFactory.New(metrics.ResourceValidationMetricName).Done(err)() + if err != nil { + logger.Error(fmt.Sprintf("Error validating %s received from ECS", AttachResourceMessageName), logger.Fields{ + field.Error: err, + }) + return + } + + messageID := aws.StringValue(message.MessageId) + expiresAt := receivedAt.Add( + time.Duration(aws.Int64Value(message.WaitTimeoutMs)) * time.Millisecond) + go r.resourceHandler.HandleResourceAttachment(&resource.ResourceAttachment{ + AttachmentInfo: attachmentinfo.AttachmentInfo{ + TaskARN: aws.StringValue(message.TaskArn), + TaskClusterARN: aws.StringValue(message.TaskClusterArn), + ClusterARN: aws.StringValue(message.ClusterArn), + ContainerInstanceARN: aws.StringValue(message.ContainerInstanceArn), + ExpiresAt: expiresAt, + AttachmentARN: aws.StringValue(message.Attachment.AttachmentArn), + Status: status.AttachmentNone, + }, + AttachmentProperties: attachmentProperties, + }) + + // Send ACK. + go func() { + err := r.respond(&ecsacs.AckRequest{ + Cluster: message.ClusterArn, + ContainerInstance: message.ContainerInstanceArn, + MessageId: message.MessageId, + }) + if err != nil { + logger.Warn(fmt.Sprintf("Error acknowledging %s", AttachResourceMessageName), logger.Fields{ + field.MessageID: messageID, + field.Error: err, + }) + } + }() +} + +// validateAttachResourceMessage performs validation checks on the ConfirmAttachmentMessage +// and returns the attachment properties received from validateAttachmentAndReturnProperties() +func validateAttachResourceMessage(message *ecsacs.ConfirmAttachmentMessage) ( + attachmentProperties map[string]string, err error) { + if message == nil { + return nil, errors.New("Message is empty") + } + + messageID := aws.StringValue(message.MessageId) + if messageID == "" { + return nil, errors.New("Message ID is not set") + } + + clusterArn := aws.StringValue(message.ClusterArn) + _, err = awsARN.Parse(clusterArn) + if err != nil { + return nil, errors.Errorf("Invalid clusterArn specified for message ID %s", messageID) + } + + containerInstanceArn := aws.StringValue(message.ContainerInstanceArn) + _, err = awsARN.Parse(containerInstanceArn) + if err != nil { + return nil, errors.Errorf( + "Invalid containerInstanceArn specified for message ID %s", messageID) + } + + attachment := message.Attachment + if attachment == nil { + return nil, errors.Errorf( + "No resource attachment for message ID %s", messageID) + } + + attachmentProperties, err = validateAttachmentAndReturnProperties(message) + if err != nil { + return nil, errors.Wrap(err, "unable to validate resource") + } + + return attachmentProperties, nil +} + +// validateAttachment performs validation checks on the attachment contained in the ConfirmAttachmentMessage +// and returns the attachment's properties +func validateAttachmentAndReturnProperties(message *ecsacs.ConfirmAttachmentMessage) ( + attachmentProperties map[string]string, err error) { + attachment := message.Attachment + + arn := aws.StringValue(attachment.AttachmentArn) + _, err = awsARN.Parse(arn) + if err != nil { + return nil, errors.Errorf( + "resource attachment validation: invalid arn %s specified for attachment: %s", arn, attachment.String()) + } + + attachmentProperties = make(map[string]string) + properties := attachment.AttachmentProperties + for _, property := range properties { + name := aws.StringValue(property.Name) + if name == "" { + return nil, errors.Errorf( + "resource attachment validation: no name specified for attachment property: %s", property.String()) + } + + value := aws.StringValue(property.Value) + if value == "" { + return nil, errors.Errorf( + "resource attachment validation: no value specified for attachment property: %s", property.String()) + } + + attachmentProperties[name] = value + } + + err = resource.ValidateResource(attachmentProperties) + if err != nil { + return nil, errors.Wrap(err, "resource attachment validation error") + } + + return attachmentProperties, nil +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_attachment.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_attachment.go new file mode 100644 index 0000000000..83857dc73c --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_attachment.go @@ -0,0 +1,85 @@ +// 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 ( + "github.com/aws/amazon-ecs-agent/ecs-agent/api/attachmentinfo" +) + +// The ResourceAttachment is a general attachment for resources created specifically for Fargate launch type +// (e.g., EBS volume, V2N). +type ResourceAttachment struct { + attachmentinfo.AttachmentInfo + // 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"` +} + +// 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 +) + +// 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 +} + +// 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 +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_type.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_type.go new file mode 100644 index 0000000000..9dbd4b43c3 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_type.go @@ -0,0 +1,19 @@ +// 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 + +const ( + EphemeralStorage = "EphemeralStorage" + ElasticBlockStorage = "ElasticBlockStorage" +) diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_validation.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_validation.go new file mode 100644 index 0000000000..ea35ae7f67 --- /dev/null +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/api/resource/resource_validation.go @@ -0,0 +1,78 @@ +// 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 ( + "github.com/pkg/errors" +) + +// ValidateResource checks if the provided resource type is valid, as well as if the attachment +// properties of the specified resource are valid. +func ValidateResource(resourceAttachmentProperties map[string]string) error { + resourceType, ok := resourceAttachmentProperties[ResourceTypeName] + if !ok { + return errors.New("resource attachment validation: no resourceType found") + } + + err := validateCommonAttachmentProperties(resourceAttachmentProperties) + if err != nil { + return errors.Wrapf(err, "failed to validate resource type %s", resourceType) + } + + switch resourceType { + case EphemeralStorage: + err = validateEphemeralStorageProperties(resourceAttachmentProperties) + case ElasticBlockStorage: + err = validateVolumeAttachmentProperties(resourceAttachmentProperties) + default: + return errors.Errorf("unknown resourceType provided: %s", resourceType) + } + if err != nil { + return errors.Wrapf(err, "failed to validate resource type %s", resourceType) + } + return nil +} + +func validateEphemeralStorageProperties(properties map[string]string) error { + err := validateVolumeAttachmentProperties(properties) + if err != nil { + return err + } + for _, property := range getExtensibleEphemeralStorageProperties() { + if _, ok := properties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} + +// validateCommonAttachmentProperties checks if the required common properties exist for an attachment +func validateCommonAttachmentProperties(resourceAttachmentProperties map[string]string) error { + for _, property := range getCommonProperties() { + if _, ok := resourceAttachmentProperties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} + +// validateVolumeAttachmentProperties checks if the required properties exist for a given volume attachment. +func validateVolumeAttachmentProperties(volumeAttachmentProperties map[string]string) error { + for _, property := range getVolumeSpecificProperties() { + if _, ok := volumeAttachmentProperties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} diff --git a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/metrics/constants.go b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/metrics/constants.go index 5dfe4bb5c2..d3ada8cee8 100644 --- a/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/metrics/constants.go +++ b/agent/vendor/github.com/aws/amazon-ecs-agent/ecs-agent/metrics/constants.go @@ -22,4 +22,8 @@ const ( GetTaskProtectionMetricName = metadataServerMetricNamespace + ".GetTaskProtection" UpdateTaskProtectionMetricName = metadataServerMetricNamespace + ".UpdateTaskProtection" AuthConfigMetricName = metadataServerMetricNamespace + ".AuthConfig" + + // AttachResourceResponder + attachResourceResponderNamespace = "ResourceAttachment" + ResourceValidationMetricName = attachResourceResponderNamespace + ".Validation" ) diff --git a/agent/vendor/modules.txt b/agent/vendor/modules.txt index 70873dcc52..f9422ca91a 100644 --- a/agent/vendor/modules.txt +++ b/agent/vendor/modules.txt @@ -17,6 +17,7 @@ github.com/aws/amazon-ecs-agent/ecs-agent/api/attachmentinfo github.com/aws/amazon-ecs-agent/ecs-agent/api/eni github.com/aws/amazon-ecs-agent/ecs-agent/api/errors github.com/aws/amazon-ecs-agent/ecs-agent/api/mocks +github.com/aws/amazon-ecs-agent/ecs-agent/api/resource github.com/aws/amazon-ecs-agent/ecs-agent/api/status github.com/aws/amazon-ecs-agent/ecs-agent/credentials github.com/aws/amazon-ecs-agent/ecs-agent/credentials/mocks diff --git a/ecs-agent/acs/session/attach_resource_responder.go b/ecs-agent/acs/session/attach_resource_responder.go new file mode 100644 index 0000000000..4ad3ba64c3 --- /dev/null +++ b/ecs-agent/acs/session/attach_resource_responder.go @@ -0,0 +1,189 @@ +// 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 session + +import ( + "fmt" + "time" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/attachmentinfo" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/resource" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/status" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger" + "github.com/aws/amazon-ecs-agent/ecs-agent/logger/field" + "github.com/aws/amazon-ecs-agent/ecs-agent/metrics" + "github.com/aws/amazon-ecs-agent/ecs-agent/wsclient" + + "github.com/aws/aws-sdk-go/aws" + awsARN "github.com/aws/aws-sdk-go/aws/arn" + "github.com/pkg/errors" +) + +const ( + AttachResourceMessageName = "ConfirmAttachmentMessage" +) + +type ResourceHandler interface { + HandleResourceAttachment(Attachment *resource.ResourceAttachment) +} + +// attachResourceResponder implements the wsclient.RequestResponder interface for responding +// to ecsacs.ConfirmAttachmentMessage messages sent by ACS. +type attachResourceResponder struct { + resourceHandler ResourceHandler + metricsFactory metrics.EntryFactory + respond wsclient.RespondFunc +} + +func NewAttachResourceResponder(resourceHandler ResourceHandler, metricsFactory metrics.EntryFactory, + responseSender wsclient.RespondFunc) wsclient.RequestResponder { + r := &attachResourceResponder{ + resourceHandler: resourceHandler, + metricsFactory: metricsFactory, + } + r.respond = ResponseToACSSender(r.Name(), responseSender) + return r +} + +func (*attachResourceResponder) Name() string { return "attach resource responder" } + +func (r *attachResourceResponder) HandlerFunc() wsclient.RequestHandler { + return r.handleAttachMessage +} + +func (r *attachResourceResponder) handleAttachMessage(message *ecsacs.ConfirmAttachmentMessage) { + logger.Debug(fmt.Sprintf("Handling %s", AttachResourceMessageName)) + receivedAt := time.Now() + + // Validate fields in the message. + attachmentProperties, err := validateAttachResourceMessage(message) + r.metricsFactory.New(metrics.ResourceValidationMetricName).Done(err)() + if err != nil { + logger.Error(fmt.Sprintf("Error validating %s received from ECS", AttachResourceMessageName), logger.Fields{ + field.Error: err, + }) + return + } + + messageID := aws.StringValue(message.MessageId) + expiresAt := receivedAt.Add( + time.Duration(aws.Int64Value(message.WaitTimeoutMs)) * time.Millisecond) + go r.resourceHandler.HandleResourceAttachment(&resource.ResourceAttachment{ + AttachmentInfo: attachmentinfo.AttachmentInfo{ + TaskARN: aws.StringValue(message.TaskArn), + TaskClusterARN: aws.StringValue(message.TaskClusterArn), + ClusterARN: aws.StringValue(message.ClusterArn), + ContainerInstanceARN: aws.StringValue(message.ContainerInstanceArn), + ExpiresAt: expiresAt, + AttachmentARN: aws.StringValue(message.Attachment.AttachmentArn), + Status: status.AttachmentNone, + }, + AttachmentProperties: attachmentProperties, + }) + + // Send ACK. + go func() { + err := r.respond(&ecsacs.AckRequest{ + Cluster: message.ClusterArn, + ContainerInstance: message.ContainerInstanceArn, + MessageId: message.MessageId, + }) + if err != nil { + logger.Warn(fmt.Sprintf("Error acknowledging %s", AttachResourceMessageName), logger.Fields{ + field.MessageID: messageID, + field.Error: err, + }) + } + }() +} + +// validateAttachResourceMessage performs validation checks on the ConfirmAttachmentMessage +// and returns the attachment properties received from validateAttachmentAndReturnProperties() +func validateAttachResourceMessage(message *ecsacs.ConfirmAttachmentMessage) ( + attachmentProperties map[string]string, err error) { + if message == nil { + return nil, errors.New("Message is empty") + } + + messageID := aws.StringValue(message.MessageId) + if messageID == "" { + return nil, errors.New("Message ID is not set") + } + + clusterArn := aws.StringValue(message.ClusterArn) + _, err = awsARN.Parse(clusterArn) + if err != nil { + return nil, errors.Errorf("Invalid clusterArn specified for message ID %s", messageID) + } + + containerInstanceArn := aws.StringValue(message.ContainerInstanceArn) + _, err = awsARN.Parse(containerInstanceArn) + if err != nil { + return nil, errors.Errorf( + "Invalid containerInstanceArn specified for message ID %s", messageID) + } + + attachment := message.Attachment + if attachment == nil { + return nil, errors.Errorf( + "No resource attachment for message ID %s", messageID) + } + + attachmentProperties, err = validateAttachmentAndReturnProperties(message) + if err != nil { + return nil, errors.Wrap(err, "unable to validate resource") + } + + return attachmentProperties, nil +} + +// validateAttachment performs validation checks on the attachment contained in the ConfirmAttachmentMessage +// and returns the attachment's properties +func validateAttachmentAndReturnProperties(message *ecsacs.ConfirmAttachmentMessage) ( + attachmentProperties map[string]string, err error) { + attachment := message.Attachment + + arn := aws.StringValue(attachment.AttachmentArn) + _, err = awsARN.Parse(arn) + if err != nil { + return nil, errors.Errorf( + "resource attachment validation: invalid arn %s specified for attachment: %s", arn, attachment.String()) + } + + attachmentProperties = make(map[string]string) + properties := attachment.AttachmentProperties + for _, property := range properties { + name := aws.StringValue(property.Name) + if name == "" { + return nil, errors.Errorf( + "resource attachment validation: no name specified for attachment property: %s", property.String()) + } + + value := aws.StringValue(property.Value) + if value == "" { + return nil, errors.Errorf( + "resource attachment validation: no value specified for attachment property: %s", property.String()) + } + + attachmentProperties[name] = value + } + + err = resource.ValidateResource(attachmentProperties) + if err != nil { + return nil, errors.Wrap(err, "resource attachment validation error") + } + + return attachmentProperties, nil +} diff --git a/ecs-agent/acs/session/attach_resource_responder_test.go b/ecs-agent/acs/session/attach_resource_responder_test.go new file mode 100644 index 0000000000..c9cb49b78b --- /dev/null +++ b/ecs-agent/acs/session/attach_resource_responder_test.go @@ -0,0 +1,147 @@ +//go:build unit +// +build unit + +// 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 session + +import ( + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/model/ecsacs" + "github.com/aws/amazon-ecs-agent/ecs-agent/acs/session/testconst" + "github.com/aws/amazon-ecs-agent/ecs-agent/api/resource" +) + +const ( + testAttachmentArn = "arn:aws:ecs:us-west-2:123456789012:ephemeral-storage/a1b2c3d4-5678-90ab-cdef-11111EXAMPLE" + testClusterArn = "arn:aws:ecs:us-west-2:123456789012:cluster/a1b2c3d4-5678-90ab-cdef-11111EXAMPLE" +) + +var ( + testAttachmentProperties = []*ecsacs.AttachmentProperty{ + { + Name: aws.String(resource.FargateResourceIdName), + Value: aws.String("name1"), + }, + { + Name: aws.String(resource.VolumeIdName), + Value: aws.String("id1"), + }, + { + Name: aws.String(resource.VolumeSizeInGiBName), + Value: aws.String("size1"), + }, + { + Name: aws.String(resource.RequestedSizeName), + Value: aws.String("size2"), + }, + { + Name: aws.String(resource.ResourceTypeName), + Value: aws.String(resource.EphemeralStorage), + }, + { + Name: aws.String(resource.DeviceName), + Value: aws.String("device1"), + }, + } + testAttachment = &ecsacs.Attachment{ + AttachmentArn: aws.String(testAttachmentArn), + AttachmentProperties: testAttachmentProperties, + } + testConfirmAttachmentMessage = &ecsacs.ConfirmAttachmentMessage{ + Attachment: testAttachment, + MessageId: aws.String(testconst.MessageID), + ClusterArn: aws.String(testClusterArn), + ContainerInstanceArn: aws.String(testconst.ContainerInstanceARN), + TaskArn: aws.String(testconst.TaskARN), + WaitTimeoutMs: aws.Int64(testconst.WaitTimeoutMillis), + } +) + +func TestValidateAttachResourceMessage(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + _, err := validateAttachResourceMessage(nil) + + require.Error(t, err) + + confirmAttachmentMessageCopy := *testConfirmAttachmentMessage + confirmAttachmentMessageCopy.Attachment = nil + + _, err = validateAttachResourceMessage(&confirmAttachmentMessageCopy) + + require.Error(t, err) + + confirmAttachmentMessageCopy = *testConfirmAttachmentMessage + confirmAttachmentMessageCopy.MessageId = aws.String("") + + _, err = validateAttachResourceMessage(&confirmAttachmentMessageCopy) + + require.Error(t, err) + + confirmAttachmentMessageCopy = *testConfirmAttachmentMessage + confirmAttachmentMessageCopy.ClusterArn = aws.String("") + + _, err = validateAttachResourceMessage(&confirmAttachmentMessageCopy) + + require.Error(t, err) + + confirmAttachmentMessageCopy = *testConfirmAttachmentMessage + confirmAttachmentMessageCopy.ContainerInstanceArn = aws.String("") + + _, err = validateAttachResourceMessage(&confirmAttachmentMessageCopy) + + require.Error(t, err) +} + +func TestValidateAttachmentAndReturnProperties(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + confirmAttachmentMessageCopy := *testConfirmAttachmentMessage + + confirmAttachmentMessageCopy.Attachment.AttachmentArn = aws.String("incorrectArn") + _, err := validateAttachmentAndReturnProperties(&confirmAttachmentMessageCopy) + require.Error(t, err) + confirmAttachmentMessageCopy.Attachment.AttachmentArn = aws.String(testAttachmentArn) + + for _, property := range confirmAttachmentMessageCopy.Attachment.AttachmentProperties { + t.Run(property.String(), func(t *testing.T) { + originalPropertyName := property.Name + property.Name = aws.String("") + _, err := validateAttachmentAndReturnProperties(&confirmAttachmentMessageCopy) + require.Error(t, err) + property.Name = originalPropertyName + + originalPropertyValue := property.Value + property.Value = aws.String("") + _, err = validateAttachmentAndReturnProperties(&confirmAttachmentMessageCopy) + require.Error(t, err) + property.Value = originalPropertyValue + + if aws.StringValue(originalPropertyName) == resource.ResourceTypeName { + property.Name = aws.String("not resourceType") + _, err = validateAttachmentAndReturnProperties(&confirmAttachmentMessageCopy) + require.Error(t, err) + property.Name = originalPropertyName + } + }) + } +} diff --git a/ecs-agent/api/resource/resource_attachment.go b/ecs-agent/api/resource/resource_attachment.go new file mode 100644 index 0000000000..83857dc73c --- /dev/null +++ b/ecs-agent/api/resource/resource_attachment.go @@ -0,0 +1,85 @@ +// 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 ( + "github.com/aws/amazon-ecs-agent/ecs-agent/api/attachmentinfo" +) + +// The ResourceAttachment is a general attachment for resources created specifically for Fargate launch type +// (e.g., EBS volume, V2N). +type ResourceAttachment struct { + attachmentinfo.AttachmentInfo + // 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"` +} + +// 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 +) + +// 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 +} + +// 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 +} diff --git a/ecs-agent/api/resource/resource_type.go b/ecs-agent/api/resource/resource_type.go new file mode 100644 index 0000000000..9dbd4b43c3 --- /dev/null +++ b/ecs-agent/api/resource/resource_type.go @@ -0,0 +1,19 @@ +// 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 + +const ( + EphemeralStorage = "EphemeralStorage" + ElasticBlockStorage = "ElasticBlockStorage" +) diff --git a/ecs-agent/api/resource/resource_validation.go b/ecs-agent/api/resource/resource_validation.go new file mode 100644 index 0000000000..ea35ae7f67 --- /dev/null +++ b/ecs-agent/api/resource/resource_validation.go @@ -0,0 +1,78 @@ +// 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 ( + "github.com/pkg/errors" +) + +// ValidateResource checks if the provided resource type is valid, as well as if the attachment +// properties of the specified resource are valid. +func ValidateResource(resourceAttachmentProperties map[string]string) error { + resourceType, ok := resourceAttachmentProperties[ResourceTypeName] + if !ok { + return errors.New("resource attachment validation: no resourceType found") + } + + err := validateCommonAttachmentProperties(resourceAttachmentProperties) + if err != nil { + return errors.Wrapf(err, "failed to validate resource type %s", resourceType) + } + + switch resourceType { + case EphemeralStorage: + err = validateEphemeralStorageProperties(resourceAttachmentProperties) + case ElasticBlockStorage: + err = validateVolumeAttachmentProperties(resourceAttachmentProperties) + default: + return errors.Errorf("unknown resourceType provided: %s", resourceType) + } + if err != nil { + return errors.Wrapf(err, "failed to validate resource type %s", resourceType) + } + return nil +} + +func validateEphemeralStorageProperties(properties map[string]string) error { + err := validateVolumeAttachmentProperties(properties) + if err != nil { + return err + } + for _, property := range getExtensibleEphemeralStorageProperties() { + if _, ok := properties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} + +// validateCommonAttachmentProperties checks if the required common properties exist for an attachment +func validateCommonAttachmentProperties(resourceAttachmentProperties map[string]string) error { + for _, property := range getCommonProperties() { + if _, ok := resourceAttachmentProperties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} + +// validateVolumeAttachmentProperties checks if the required properties exist for a given volume attachment. +func validateVolumeAttachmentProperties(volumeAttachmentProperties map[string]string) error { + for _, property := range getVolumeSpecificProperties() { + if _, ok := volumeAttachmentProperties[property]; !ok { + return errors.Errorf("property %s not found in attachment properties", property) + } + } + return nil +} diff --git a/ecs-agent/api/resource/resource_validation_test.go b/ecs-agent/api/resource/resource_validation_test.go new file mode 100644 index 0000000000..ac8fe0fd22 --- /dev/null +++ b/ecs-agent/api/resource/resource_validation_test.go @@ -0,0 +1,85 @@ +//go:build unit +// +build unit + +// 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 ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestValidateVolumeResource tests that validation for volume resource attachments takes place properly. +func TestValidateVolumeResource(t *testing.T) { + volumeAttachmentProperties := make(map[string]string) + testString := "(╯°□°)╯︵ ┻━┻" + + for _, property := range getCommonProperties() { + volumeAttachmentProperties[property] = testString + } + for _, property := range getVolumeSpecificProperties() { + volumeAttachmentProperties[property] = testString + } + + for _, tc := range []struct { + resourceType string + // resourceSpecificProperties is the list of properties specific to a resource type. + resourceSpecificProperties []string + }{ + { + resourceType: EphemeralStorage, + resourceSpecificProperties: getExtensibleEphemeralStorageProperties(), + }, + { + resourceType: ElasticBlockStorage, + // ElasticBlockStorage resource type does not have any specific properties. + resourceSpecificProperties: []string{}, + }, + } { + t.Run(tc.resourceType, func(t *testing.T) { + resourceProperties := make(map[string]string, len(volumeAttachmentProperties)) + for k, v := range volumeAttachmentProperties { + resourceProperties[k] = v + } + + resourceProperties[ResourceTypeName] = tc.resourceType + for _, property := range tc.resourceSpecificProperties { + resourceProperties[property] = testString + } + + err := ValidateResource(resourceProperties) + require.NoError(t, err) + + // `requiredProperties` contains all properties required for a resource. + // When any item from requiredProperties is missing from the resource's attachmentProperties, + // we expect resource validation to fail. + requiredProperties := make([]string, 0) + for key := range resourceProperties { + requiredProperties = append(requiredProperties, key) + } + // Test that we are validating for all properties by removing each property one at a time, and then resetting. + for _, property := range requiredProperties { + // Store the current value so that we can add it back when we reset after removing it. + val := resourceProperties[property] + delete(resourceProperties, property) + err := ValidateResource(resourceProperties) + require.Error(t, err) + // Make resourceProperties whole again. + resourceProperties[property] = val + } + }) + } +} diff --git a/ecs-agent/metrics/constants.go b/ecs-agent/metrics/constants.go index 5dfe4bb5c2..d3ada8cee8 100644 --- a/ecs-agent/metrics/constants.go +++ b/ecs-agent/metrics/constants.go @@ -22,4 +22,8 @@ const ( GetTaskProtectionMetricName = metadataServerMetricNamespace + ".GetTaskProtection" UpdateTaskProtectionMetricName = metadataServerMetricNamespace + ".UpdateTaskProtection" AuthConfigMetricName = metadataServerMetricNamespace + ".AuthConfig" + + // AttachResourceResponder + attachResourceResponderNamespace = "ResourceAttachment" + ResourceValidationMetricName = attachResourceResponderNamespace + ".Validation" )