Skip to content

Commit

Permalink
Implemented new command to check if a set of container instances have…
Browse files Browse the repository at this point in the history
… the attributes required to run a given task definition
  • Loading branch information
nithu0115 authored and PettitWesley committed Mar 1, 2019
1 parent be660fb commit 12483e0
Show file tree
Hide file tree
Showing 8 changed files with 244 additions and 1 deletion.
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -5,4 +5,6 @@ bin/
*.orig
ecs-cli/vendor/pkg
.vscode/*
ecs-cli/.vscode/*
ecs-cli/.idea
.idea/*
2 changes: 2 additions & 0 deletions ecs-cli/main.go
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/version"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/attributechecker"
)

func main() {
Expand All @@ -54,6 +55,7 @@ func main() {
imageCommand.ImagesCommand(),
licenseCommand.LicenseCommand(),
composeCommand.ComposeCommand(composeFactory),
attributecheckercommand.AttributecheckerCommand(),
logsCommand.LogCommand(),
regcredsCommand.RegistryCredsCommand(),
}
Expand Down
172 changes: 172 additions & 0 deletions ecs-cli/modules/cli/attributechecker/attribute_checker_app.go
@@ -0,0 +1,172 @@
// Copyright 2015-2017 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 attributechecker

import (
"fmt"
"os"
"strings"

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/aws-sdk-go/aws"
"github.com/docker/libcompose/project"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)

const (
displayTitle = true
containerInstanceHeader = "Container Instance"
missingAttributesHeader = "Missing Attributes"
)

var infoColumns = []string{containerInstanceHeader, missingAttributesHeader}

//AttributeChecker will compare task def and containers instances attributes and outputs missing attributes
func AttributeChecker(c *cli.Context) {
err := validateAttributeCheckerFlags(c)
if err != nil {
logrus.Fatal("Error executing 'Attribute Checker': ", err)
}
rdwr, err := config.NewReadWriter()
if err != nil {
logrus.Fatal("Error executing 'Attribute Checker': ", err)
}
commandConfig, err := config.NewCommandConfig(c, rdwr)
if err != nil {
logrus.Fatal("Error executing 'Attribute Checker': ", err)
}

ecsClient := ecsclient.NewECSClient(commandConfig)

taskDefAttributeNames, err := taskdefattributesCheckRequest(c, ecsClient)
if err != nil {
logrus.Fatal("Error executing 'Attribute Checker': ", err)
}
if len(taskDefAttributeNames) == 0 {
logrus.Info("The given task definition does not have any attributes")
return
}

descrContainerInstancesResponse, err := describeContainerInstancesAttributeMap(c, ecsClient, commandConfig)
if err != nil {
logrus.Fatal("Error executing 'Attribute Checker': ", err)
}

compareOutput := compare(taskDefAttributeNames, descrContainerInstancesResponse)
result := ConvertToInfoSet(compareOutput)
os.Stdout.WriteString(result.String(infoColumns, displayTitle))
}

func contains(containerInstanceAttributeNames []*string, tdAttrNames *string) bool {
for _, containerInstAttrNames := range containerInstanceAttributeNames {
if *containerInstAttrNames == *tdAttrNames {
return true
}
}
return false
}

//compares between container instances and Task definition
func compare(taskDefAttributeNames []*string, descrContainerInstancesResponse map[string][]*string) map[string]string {
attributeCheckerResult := make(map[string]string)
for containerInstanceARN, containerInstanceAttributeNames := range descrContainerInstancesResponse {
var missingAttributes []string
for _, tdAttrNames := range taskDefAttributeNames {
if !contains(containerInstanceAttributeNames, tdAttrNames) {
missingAttributes = append(missingAttributes, *tdAttrNames)
}
}
missingAttributesNames := strings.Join(missingAttributes, ", ")
if len(missingAttributesNames) == 0 {
missingAttributesNames = "None"
}
containerInstance := strings.Split(containerInstanceARN, "/")
attributeCheckerResult[containerInstance[1]] = missingAttributesNames
}
return attributeCheckerResult
}

// DescribeContainerInstancesAttributeMap and get a map with Container instance ARN and Container instances attribute Names
func describeContainerInstancesAttributeMap(context *cli.Context, ecsClient ecsclient.ECSClient, commandConfig *config.CommandConfig) (map[string][]*string, error) {
if err := validateCluster(commandConfig.Cluster, ecsClient); err != nil {
return nil, err
}
var containerInstanceIdentifiers []*string
containerInstanceIdentifier := context.String(flags.ContainerInstancesFlag)
splitValues := strings.Split(containerInstanceIdentifier, ",")
containerInstanceIdentifiers = aws.StringSlice(splitValues)

descrContainerInstancesAttributes, err := ecsClient.GetAttributesFromDescribeContainerInstances(containerInstanceIdentifiers)
if err != nil {
return nil, errors.Wrapf(err, fmt.Sprintf("Failed to Describe Container Instances, please check region/containerInstance values"))
}
return descrContainerInstancesAttributes, err
}

// validateCluster validates if the cluster exists in ECS and is in "ACTIVE" state.
func validateCluster(clusterName string, ecsClient ecsclient.ECSClient) error {
isClusterActive, err := ecsClient.IsActiveCluster(clusterName)
if err != nil {
return err
}

if !isClusterActive {
return fmt.Errorf("Cluster '%s' is not active. Ensure that it exists", clusterName)
}
return nil
}

//taskdefattributesCheckRequest describes task def and gets all attribute Names from the task definition
func taskdefattributesCheckRequest(context *cli.Context, ecsClient ecsclient.ECSClient) ([]*string, error) {

taskDefIdentifier := context.String(flags.TaskDefinitionFlag)

descrTaskDefinition, err := ecsClient.DescribeTaskDefinition(taskDefIdentifier)
if err != nil {
return nil, errors.Wrapf(err, fmt.Sprintf("Failed to Describe TaskDefinition, please check the region/taskDefinition values"))
}
var taskattributeNames []*string
for _, taskDefAttributesName := range descrTaskDefinition.RequiresAttributes {
taskattributeNames = append(taskattributeNames, taskDefAttributesName.Name)
}
return taskattributeNames, err
}

//validates all required flags are passed to run the command
func validateAttributeCheckerFlags(context *cli.Context) error {
if taskDefIdentifier := context.String(flags.TaskDefinitionFlag); taskDefIdentifier == "" {
return fmt.Errorf("TaskDefinition must be specified with the --%s flag", flags.TaskDefinitionFlag)
}
if containerInstanceIdentifier := context.String(flags.ContainerInstancesFlag); containerInstanceIdentifier == "" {
return fmt.Errorf("ContainerInstance(s) must be specified with the --%s flag", flags.ContainerInstancesFlag)
}
return nil
}

//ConvertToInfoSet transforms the Map of containerARN and MissingAttributes into a formatted set of fields
func ConvertToInfoSet(compareOutput map[string]string) project.InfoSet {
result := project.InfoSet{}
for key, element := range compareOutput {
info := project.Info{
containerInstanceHeader: key,
missingAttributesHeader: element,
}
result = append(result, info)
}
return result
}
1 change: 1 addition & 0 deletions ecs-cli/modules/cli/compose/container/container.go
Expand Up @@ -39,6 +39,7 @@ type Container struct {
task *ecs.Task
EC2IPAddress string
networkBindings []*ecs.NetworkBinding

ecsContainer *ecs.Container
}

Expand Down
32 changes: 32 additions & 0 deletions ecs-cli/modules/clients/aws/ecs/client.go
Expand Up @@ -60,6 +60,9 @@ type ECSClient interface {

// Container Instance related
GetEC2InstanceIDs(containerInstanceArns []*string) (map[string]string, error)

//Describe Container Instances - Attribute Checker related
GetAttributesFromDescribeContainerInstances(containerInstanceArns []*string) (map[string][]*string, error)
}

// ecsClient implements ECSClient
Expand Down Expand Up @@ -391,6 +394,35 @@ func (c *ecsClient) GetEC2InstanceIDs(containerInstanceArns []*string) (map[stri
return containerToEC2InstanceMap, nil
}

// DescribeContainer Instances returns a Map with key container instance ARN and values list of attributes
func (c *ecsClient) GetAttributesFromDescribeContainerInstances(containerInstanceArns []*string) (map[string][]*string, error) {
descrContainerInstancesoutputMap := map[string][]*string{}
for i := 0; i < len(containerInstanceArns); i += ecsChunkSize {
var chunk []*string
if i+ecsChunkSize > len(containerInstanceArns) {
chunk = containerInstanceArns[i:len(containerInstanceArns)]
} else {
chunk = containerInstanceArns[i : i+ecsChunkSize]
}

descrContainerInstances, err := c.client.DescribeContainerInstances(&ecs.DescribeContainerInstancesInput{
Cluster: aws.String(c.config.Cluster),
ContainerInstances: chunk,
})
if err != nil {
return nil, err
}
for _, containerInstance := range descrContainerInstances.ContainerInstances {
var containerInstanceAttributeNames []*string
for _, containerInstanceattributenames := range containerInstance.Attributes {
containerInstanceAttributeNames = append(containerInstanceAttributeNames, containerInstanceattributenames.Name)
}
descrContainerInstancesoutputMap[aws.StringValue(containerInstance.ContainerInstanceArn)] = containerInstanceAttributeNames
}
}
return descrContainerInstancesoutputMap, nil
}

// IsActiveCluster returns true if the cluster exists and can be described.
func (c *ecsClient) IsActiveCluster(clusterName string) (bool, error) {
output, err := c.client.DescribeClusters(&ecs.DescribeClustersInput{
Expand Down
@@ -0,0 +1,31 @@
package attributecheckercommand

import (
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/attributechecker"
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
"github.com/urfave/cli"
)

// AttributecheckerCommand checks if all Capabilities/attributes are available to run the task on a specified Cluster or on a given Container Instance specified.
func AttributecheckerCommand() cli.Command {
return cli.Command{
Name: "check-attributes",
Usage: "Checks if a given list of container instances can run a given task definition by checking their attributes. Outputs attributes that are required by the task definition but not present on the container instances.",
Flags: append(flags.OptionalConfigFlags(), attributecheckerFlags()...),
Action: attributechecker.AttributeChecker,
OnUsageError: flags.UsageErrorFactory("attribute-checker"),
}
}

func attributecheckerFlags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: flags.TaskDefinitionFlag,
Usage: "Specifies the name or full Amazon Resource Name (ARN) of the ECS Task Definition. This is required to gather attributes of a Task Definition.",
},
cli.StringFlag{
Name: flags.ContainerInstancesFlag,
Usage: "A list of container instance IDs or full ARN entries to check if all required attributes are available for the Task Definition to RunTask.",
},
}
}
2 changes: 1 addition & 1 deletion ecs-cli/modules/commands/cluster/cluster_command.go
Expand Up @@ -155,4 +155,4 @@ func clusterScaleFlags() []cli.Flag {
Usage: "Specifies the number of instances to maintain in your cluster.",
},
}
}
}
3 changes: 3 additions & 0 deletions ecs-cli/modules/commands/flags/flags.go
Expand Up @@ -83,6 +83,9 @@ const (
LaunchTypeFlag = "launch-type"
DefaultLaunchTypeFlag = "default-launch-type"
SchedulingStrategyFlag = "scheduling-strategy"

//attribute-checker
ContainerInstancesFlag = "container-instances"

// Cluster
AsgMaxSizeFlag = "size"
Expand Down

0 comments on commit 12483e0

Please sign in to comment.