diff --git a/.gitignore b/.gitignore index 423c55157..949a13041 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ ecs-cli/vendor/pkg ecs-cli/.vscode/* ecs-cli/.idea .idea/* +.DS_Store \ No newline at end of file diff --git a/ecs-cli/modules/cli/compose/entity/service/service.go b/ecs-cli/modules/cli/compose/entity/service/service.go index 96c3f4463..6beb774f2 100644 --- a/ecs-cli/modules/cli/compose/entity/service/service.go +++ b/ecs-cli/modules/cli/compose/entity/service/service.go @@ -41,15 +41,16 @@ import ( // Service type is placeholder for a single task definition and its cache // and it performs operations on ECS Service level type Service struct { - taskDef *ecs.TaskDefinition - cache cache.Cache - ecsContext *context.ECSContext - deploymentConfig *ecs.DeploymentConfiguration - loadBalancers []*ecs.LoadBalancer - role string - healthCheckGP *int64 - serviceRegistries []*ecs.ServiceRegistry - tags []*ecs.Tag + taskDef *ecs.TaskDefinition + cache cache.Cache + ecsContext *context.ECSContext + deploymentConfig *ecs.DeploymentConfiguration + loadBalancers []*ecs.LoadBalancer + capacityProviderStrategy []*ecs.CapacityProviderStrategyItem + role string + healthCheckGP *int64 + serviceRegistries []*ecs.ServiceRegistry + tags []*ecs.Tag } const ( @@ -105,6 +106,9 @@ func (s *Service) LoadContext() error { return err } + // Capacity Provider Strategy + capacityProviders := s.Context().CLIContext.StringSlice(flags.CapacityProviderStrategyFlag) + // Health Check Grace Period healthCheckGP, err := getInt64FromCLIContext(s.Context(), flags.HealthCheckGracePeriodFlag) if err != nil { @@ -153,6 +157,15 @@ func (s *Service) LoadContext() error { } s.loadBalancers = append(s.loadBalancers, loadBalancers...) } + + if len(capacityProviders) != 0 { + capacityProviderStrategy, err := utils.ParseCapacityProviders(capacityProviders) + if err != nil { + return err + } + s.capacityProviderStrategy = append(s.capacityProviderStrategy, capacityProviderStrategy...) + } + s.role = role return nil } @@ -569,7 +582,11 @@ func (s *Service) buildCreateServiceInput(serviceName, taskDefName string, desir createServiceInput.PlacementStrategy = placementStrategy } - if launchType != "" { + // just let capacity provider take precedence if it is set + // otherwise, fall back on the launch type + if len(s.capacityProviderStrategy) > 0 { + createServiceInput.CapacityProviderStrategy = s.capacityProviderStrategy + } else if launchType != "" { createServiceInput.LaunchType = aws.String(launchType) } diff --git a/ecs-cli/modules/commands/compose/service/compose_service_command.go b/ecs-cli/modules/commands/compose/service/compose_service_command.go index 887d02aab..92dcf7bb9 100644 --- a/ecs-cli/modules/commands/compose/service/compose_service_command.go +++ b/ecs-cli/modules/commands/compose/service/compose_service_command.go @@ -68,7 +68,7 @@ func createServiceCommand(factory composeFactory.ProjectFactory) cli.Command { Name: "create", Usage: usage.ServiceCreate, Action: compose.WithProject(factory, compose.ProjectCreate, true), - Flags: flags.AppendFlags(deploymentConfigFlags(true), loadBalancerFlags(), flags.OptionalConfigFlags(), flags.OptionalLaunchTypeFlag(), flags.OptionalCreateLogsFlag(), serviceDiscoveryFlags(), flags.OptionalSchedulingStrategyFlag(), taggingFlags()), + Flags: flags.AppendFlags(deploymentConfigFlags(true), loadBalancerFlags(), capacityProviderStrategyFlags(), flags.OptionalConfigFlags(), flags.OptionalLaunchTypeFlag(), flags.OptionalCreateLogsFlag(), serviceDiscoveryFlags(), flags.OptionalSchedulingStrategyFlag(), taggingFlags()), OnUsageError: flags.UsageErrorFactory("create"), } } @@ -88,7 +88,7 @@ func upServiceCommand(factory composeFactory.ProjectFactory) cli.Command { Name: "up", Usage: usage.ServiceUp, Action: compose.WithProject(factory, compose.ProjectUp, true), - Flags: flags.AppendFlags(deploymentConfigFlags(true), loadBalancerFlags(), flags.OptionalConfigFlags(), ComposeServiceTimeoutFlag(), flags.OptionalLaunchTypeFlag(), flags.OptionalCreateLogsFlag(), ForceNewDeploymentFlag(), serviceDiscoveryFlags(), updateServiceDiscoveryFlags(), flags.OptionalSchedulingStrategyFlag(), taggingFlags()), + Flags: flags.AppendFlags(deploymentConfigFlags(true), loadBalancerFlags(), capacityProviderStrategyFlags(), flags.OptionalConfigFlags(), ComposeServiceTimeoutFlag(), flags.OptionalLaunchTypeFlag(), flags.OptionalCreateLogsFlag(), ForceNewDeploymentFlag(), serviceDiscoveryFlags(), updateServiceDiscoveryFlags(), flags.OptionalSchedulingStrategyFlag(), taggingFlags()), OnUsageError: flags.UsageErrorFactory("up"), } } @@ -226,7 +226,7 @@ func loadBalancerFlags() []cli.Flag { containerNameUsageString := fmt.Sprintf("[Deprecated] Specifies the container name (as it appears in a container definition). This parameter is required if --%s or --%s is specified.", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) containerPortUsageString := fmt.Sprintf("[Deprecated] Specifies the port on the container to associate with the load balancer. This port must correspond to a containerPort in the service's task definition. This parameter is required if --%s or --%s is specified.", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) loadBalancerNameUsageString := fmt.Sprintf("[Deprecated] Specifies the name of a previously configured Classic Elastic Load Balancing load balancer to associate with your service. NOTE: For Application Load Balancers or Network Load Balancers, use the --%s flag.", flags.TargetGroupArnFlag) - targetGroupsUsageString := fmt.Sprintf("[Optional] Specifies multiple target groups to register with a service. Can't be used with --%s flag or --%s at the same time. To specify multiple target groups, add multiple seperate --%s flags Example: ecs-cli compose service create --target-groups targetGroupArn=arn,containerName=nginx,containerPort=80 --target-groups targetGroupArn=arn,containerName=database,containerPort=3306", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag, flags.TargetGroupsFlag) + targetGroupsUsageString := fmt.Sprintf("[Optional] Specifies multiple target groups to register with a service. Can't be used with --%s flag or --%s at the same time. To specify multiple target groups, add multiple separate --%s flags Example: ecs-cli compose service create --target-groups targetGroupArn=arn,containerName=nginx,containerPort=80 --target-groups targetGroupArn=arn,containerName=database,containerPort=3306", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag, flags.TargetGroupsFlag) roleUsageString := fmt.Sprintf("[Optional] Specifies the name or full Amazon Resource Name (ARN) of the IAM role that allows Amazon ECS to make calls to your load balancer or target group on your behalf. This parameter requires either --%s or --%s to be specified.", flags.LoadBalancerNameFlag, flags.TargetGroupArnFlag) healthCheckGracePeriodString := "[Optional] Specifies the period of time, in seconds, that the Amazon ECS service scheduler should ignore unhealthy Elastic Load Balancing target health checks after a task has first started." @@ -263,6 +263,18 @@ func loadBalancerFlags() []cli.Flag { } } +func capacityProviderStrategyFlags() []cli.Flag { + capacityProviderStrategyUsageString := fmt.Sprintf("[Optional] Specifies multiple capacity providers to register with a service. Can't be used with --%s flag at the same time. To specify multiple capacity providers, add multiple separate --%s flags. Only one capacity provider can have a base > 0, set the base of all others to zero. Example: ecs-cli compose service create --capacity-provider capacityProviderName=t3-large-capacity-provider,base=1,weight=1 --capacity-provider capacityProviderName=t3-nano-capacity-provider,base=0,weight=5", flags.LaunchTypeFlag, flags.CapacityProviderStrategyFlag) + + return []cli.Flag{ + cli.StringSliceFlag{ + Name: flags.CapacityProviderStrategyFlag, + Usage: capacityProviderStrategyUsageString, + Value: &cli.StringSlice{}, + }, + } +} + // ComposeServiceTimeoutFlag allows user to specify a custom timeout func ComposeServiceTimeoutFlag() []cli.Flag { return []cli.Flag{ diff --git a/ecs-cli/modules/commands/flags/flags.go b/ecs-cli/modules/commands/flags/flags.go index 51967b463..b727bce0b 100644 --- a/ecs-cli/modules/commands/flags/flags.go +++ b/ecs-cli/modules/commands/flags/flags.go @@ -135,6 +135,7 @@ const ( ComposeServiceTimeOutFlag = "timeout" ForceDeploymentFlag = "force-deployment" TargetGroupsFlag = "target-groups" + CapacityProviderStrategyFlag = "capacity-provider" // Registry Creds UpdateExistingSecretsFlag = "update-existing-secrets" diff --git a/ecs-cli/modules/utils/utils.go b/ecs-cli/modules/utils/utils.go index a0da96b38..c3a07a4c3 100644 --- a/ecs-cli/modules/utils/utils.go +++ b/ecs-cli/modules/utils/utils.go @@ -27,11 +27,14 @@ import ( const ( // ECSCLIResourcePrefix is prepended to the names of resources created through the ecs-cli - ECSCLIResourcePrefix = "amazon-ecs-cli-setup-" - containerNameParamKey = "containerName" - containerPortParamKey = "containerPort" - loadBalancerNameParamKey = "loadBalancerName" - targetGroupArnParamKey = "targetGroupArn" + ECSCLIResourcePrefix = "amazon-ecs-cli-setup-" + containerNameParamKey = "containerName" + containerPortParamKey = "containerPort" + loadBalancerNameParamKey = "loadBalancerName" + targetGroupArnParamKey = "targetGroupArn" + capacityProviderNameParamKey = "capacityProviderName" + capacityProviderBaseParamKey = "base" + capacityProviderWeightParamKey = "weight" ) // InSlice checks if the given string exists in the given slice: @@ -112,6 +115,67 @@ func GetPartition(region string) string { } } +// ParseCapacityProviders parses a StringSlice array into an array of CapacityProviderItem +// When you specify a capacity provider strategy, the number of capacity providers that can be specified is limited to six. +// Input: ["capacityProviderName="...",base="...",weight=80","capacityProviderName="...",base="...",weight=40"] +func ParseCapacityProviders(flagValues []string) ([]*ecs.CapacityProviderStrategyItem, error) { + var list []*ecs.CapacityProviderStrategyItem + + if len(flagValues) > 6 { + return nil, fmt.Errorf("ECS only permits 6 Capacity providers, you provided %s", len(flagValues)) + } + + for _, flagValue := range flagValues { + m := make(map[string]string) + + validFlags := []string{capacityProviderNameParamKey, capacityProviderBaseParamKey, capacityProviderWeightParamKey} + currentFlags := map[string]bool{ + "capacityProviderName": false, + } + + keyValPairs := strings.Split(flagValue, ",") + + for _, kv := range keyValPairs { + pair := strings.SplitN(kv, "=", -1) + + if len(pair) != 2 { + return nil, fmt.Errorf("There is an (key=value) initialization error, please check to see if you are using = accordingly on %s", pair[0]) + } + key, val := pair[0], pair[1] + + if ok := contains(validFlags, key); !ok { + return nil, fmt.Errorf("[--%s] is an invalid flag", key) + } + m[key] = val + if currentFlags[key] { + return nil, fmt.Errorf("%s already exists", key) + } + currentFlags[key] = true + } + for key, value := range currentFlags { + if value == false { + return nil, fmt.Errorf("--%s must be specified", key) + } + } + + base, err := strconv.ParseInt(m["base"], 10, 64) + if err != nil { + return nil, fmt.Errorf("Failed to parse capacity provider base for capacity provider %s; set base to zero on all providers except one", m["capacityProviderName"]) + } + weight, err := strconv.ParseInt(m["weight"], 10, 64) + if err != nil { + return nil, fmt.Errorf("Failed to parse capacity provider weight for capacity provider %s", m["capacityProviderName"]) + } + + list = append(list, &ecs.CapacityProviderStrategyItem{ + CapacityProvider: aws.String(m["capacityProviderName"]), + Base: aws.Int64((base)), + Weight: aws.Int64((weight)), + }) + } + return list, nil +} + // ParseLoadBalancers parses a StringSlice array into an array of load balancers struct // Input: ["targetGroupArn="...",containerName="...",containerPort=80","targetGroupArn="...",containerName="...",containerPort=40"] func ParseLoadBalancers(flagValues []string) ([]*ecs.LoadBalancer, error) {