Skip to content

Commit

Permalink
Merge pull request #737 from aws/tagging
Browse files Browse the repository at this point in the history
Merge tagging feature branch into dev
  • Loading branch information
PettitWesley authored Mar 6, 2019
2 parents e6b2bc1 + ca3a69f commit a01264f
Show file tree
Hide file tree
Showing 46 changed files with 3,818 additions and 238 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ Line Interface](http://aws.amazon.com/cli/) product detail page.
- [Using FIPS Endpoints](#using-fips-endpoints)
- [Using Private Registry Authentication](#using-private-registry-authentication)
- [Checking for Missing Attributes and Debugging Reason: Attribute Errors](#Checking-for-Missing-Attributes-and-Debugging-Reason:-Attribute-Errors)
- [Tagging Resources](#tagging-resources)

- [Amazon ECS CLI Commands](#amazon-ecs-cli-commands)
- [Contributing to the CLI](#contributing-to-the-cli)
- [License](#license)
Expand Down Expand Up @@ -1026,6 +1028,23 @@ ae66e18e-1d46-47ff-81c5-647f0f1426ce com.amazonaws.ecs.capability.logging-drive

The command outputs a table of container instances and which attributes they are missing. In this case, the Task Definition requires the Fluentd log driver, but 2 container instances lack support for it.

### Tagging Resources

ECS CLI Commmands support a `--tags` flag which allows you to specify AWS Resource Tags in the format `key=value,key2=value2,key3=value3`. Resource tags can be used for cost allocation, automation, access control, and more. See [AWS Tagging Strategies](https://aws.amazon.com/answers/account-management/aws-tagging-strategies/) for a discussion of use cases.

#### ecs-cli up command

The ECS Cluster, and CloudFormation template with EC2 resources can be tagged. In addition, the ECS CLI will add tags to the following resources which are created by the CloudFormation template:
* VPC
* Subnets
* Internet Gateway
* Route Tables
* Security Group
* Autoscaling Group

For the autoscaling group, the ECS CLI will add a `Name` tag whose value will be `ECS Instance - <CloudFormation stack name>`, which will be propagated to your EC2 instances. You can override this behavior by specifying your own `Name` tag.


## Amazon ECS CLI Commands

For a complete list of commands, see the
Expand Down
4 changes: 3 additions & 1 deletion ecs-cli/Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 85 additions & 8 deletions ecs-cli/modules/cli/cluster/cluster_app.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,28 @@ import (
ecsclient "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/ecs"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils"
"github.com/aws/aws-sdk-go/aws"
sdkCFN "github.com/aws/aws-sdk-go/service/cloudformation"
"github.com/aws/aws-sdk-go/service/ecs"
"github.com/docker/libcompose/project"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

// user data builder can be easily mocked in tests
var newUserDataBuilder func(string) userdata.UserDataBuilder = userdata.NewBuilder
var newUserDataBuilder func(string, bool) userdata.UserDataBuilder = userdata.NewBuilder

// displayTitle flag is used to print the title for the fields
const displayTitle = true

// Values returned by the ECS Settings API
const (
ecsSettingEnabled = "enabled"
ecsSettingDisabled = "disabled"
)

const (
ParameterKeyAsgMaxSize = "AsgMaxSize"
ParameterKeyVPCAzs = "VpcAvailabilityZones"
Expand Down Expand Up @@ -233,11 +243,30 @@ func createCluster(context *cli.Context, awsClients *AWSClients, commandConfig *
deleteStack = true
}

tags := make([]*ecs.Tag, 0)
if tagVal := context.String(flags.ResourceTagsFlag); tagVal != "" {
tags, err = utils.ParseTags(tagVal, tags)
if err != nil {
return err
}
}

var containerInstanceTaggingSupported bool

if len(tags) > 0 {
// determine if container instance tagging is supported
containerInstanceTaggingSupported, err = canEnableContainerInstanceTagging(awsClients.ECSClient)
if err != nil {
return err
}
}

// Populate cfn params
cfnParams, err := cliFlagsToCfnStackParams(context, commandConfig.Cluster, launchType)
cfnParams, err := cliFlagsToCfnStackParams(context, commandConfig.Cluster, launchType, containerInstanceTaggingSupported)
if err != nil {
return err
}

cfnParams.Add(ParameterKeyCluster, commandConfig.Cluster)
if context.Bool(flags.NoAutoAssignPublicIPAddressFlag) {
cfnParams.Add(ParameterKeyAssociatePublicIPAddress, "false")
Expand Down Expand Up @@ -306,7 +335,7 @@ func createCluster(context *cli.Context, awsClients *AWSClients, commandConfig *
}

// Create ECS cluster
if _, err := ecsClient.CreateCluster(commandConfig.Cluster); err != nil {
if _, err := ecsClient.CreateCluster(commandConfig.Cluster, tags); err != nil {
return err
}

Expand All @@ -321,9 +350,12 @@ func createCluster(context *cli.Context, awsClients *AWSClients, commandConfig *
}
}
// Create cfn stack
template := cloudformation.GetClusterTemplate()
template, err := cloudformation.GetClusterTemplate(tags, stackName)
if err != nil {
return errors.Wrapf(err, "Error building cloudformation template")
}

if _, err := cfnClient.CreateStack(template, stackName, true, cfnParams); err != nil {
if _, err := cfnClient.CreateStack(template, stackName, true, cfnParams, convertToCFNTags(tags)); err != nil {
return err
}

Expand All @@ -332,6 +364,30 @@ func createCluster(context *cli.Context, awsClients *AWSClients, commandConfig *
return cfnClient.WaitUntilCreateComplete(stackName)
}

func canEnableContainerInstanceTagging(client ecsclient.ECSClient) (bool, error) {
output, err := client.ListAccountSettings(&ecs.ListAccountSettingsInput{
EffectiveSettings: aws.Bool(true),
Name: aws.String(ecs.SettingNameContainerInstanceLongArnFormat),
})
if err != nil {
return false, err
}

// This should never evaluate to true, unless there is a problem with API
// This if block ensures that the CLI does not panic in that case
if len(output.Settings) < 1 {
return false, fmt.Errorf("Received unexpected response from ECS Settings API: %s", output)
}

if aws.StringValue(output.Settings[0].Value) == ecsSettingEnabled {
logrus.Warnf("Enabling container instance tagging because %s is enabled for your identity, %s. If this is not your account default setting, your instances will fail to join your cluster. You can use the PutAccountSettingDefault API to change your account default.", ecs.SettingNameContainerInstanceLongArnFormat, aws.StringValue(output.Settings[0].PrincipalArn))
return true, nil
}

logrus.Warnf("Disabling container instance tagging because %s is not enabled for your identity, %s. You can use the PutAccountSettingDefault API to change your account default.", ecs.SettingNameContainerInstanceLongArnFormat, aws.StringValue(output.Settings[0].PrincipalArn))
return false, nil
}

func determineArchitecture(cfnParams *cloudformation.CfnStackParams) (string, error) {
architecture := amimetadata.ArchitectureTypeX86

Expand All @@ -354,6 +410,18 @@ func determineArchitecture(cfnParams *cloudformation.CfnStackParams) (string, er
return architecture, nil
}

// unfortunately go SDK lacks a unified Tag type
func convertToCFNTags(tags []*ecs.Tag) []*sdkCFN.Tag {
var cfnTags []*sdkCFN.Tag
for _, tag := range tags {
cfnTags = append(cfnTags, &sdkCFN.Tag{
Key: tag.Key,
Value: tag.Value,
})
}
return cfnTags
}

var newCommandConfig = func(context *cli.Context, rdwr config.ReadWriter) (*config.CommandConfig, error) {
return config.NewCommandConfig(context, rdwr)
}
Expand All @@ -378,7 +446,16 @@ func createEmptyCluster(context *cli.Context, ecsClient ecsclient.ECSClient, cfn
return fmt.Errorf("A CloudFormation stack already exists for the cluster '%s'.", commandConfig.Cluster)
}

if _, err := ecsClient.CreateCluster(commandConfig.Cluster); err != nil {
tags := make([]*ecs.Tag, 0)
var err error
if tagVal := context.String(flags.ResourceTagsFlag); tagVal != "" {
tags, err = utils.ParseTags(tagVal, tags)
if err != nil {
return err
}
}

if _, err := ecsClient.CreateCluster(commandConfig.Cluster, tags); err != nil {
return err
}

Expand Down Expand Up @@ -530,7 +607,7 @@ func deleteClusterPrompt(reader *bufio.Reader) error {
}

// cliFlagsToCfnStackParams converts values set for CLI flags to cloudformation stack parameters.
func cliFlagsToCfnStackParams(context *cli.Context, cluster, launchType string) (*cloudformation.CfnStackParams, error) {
func cliFlagsToCfnStackParams(context *cli.Context, cluster, launchType string, tagContainerInstances bool) (*cloudformation.CfnStackParams, error) {
cfnParams := cloudformation.NewCfnStackParams(requiredParameters)
for cliFlag, cfnParamKeyName := range flagNamesToStackParameterKeys {
cfnParamKeyValue := context.String(cliFlag)
Expand All @@ -540,7 +617,7 @@ func cliFlagsToCfnStackParams(context *cli.Context, cluster, launchType string)
}

if launchType == config.LaunchTypeEC2 {
builder := newUserDataBuilder(cluster)
builder := newUserDataBuilder(cluster, tagContainerInstances)
// handle extra user data, which is a string slice flag
if userDataFiles := context.StringSlice(flags.UserDataFlag); len(userDataFiles) > 0 {
for _, file := range userDataFiles {
Expand Down
Loading

0 comments on commit a01264f

Please sign in to comment.