Skip to content

Commit 922ca35

Browse files
committed
Enable Service Discovery and create route53 resources in the compose service up/create commands
1 parent 887b919 commit 922ca35

File tree

14 files changed

+515
-68
lines changed

14 files changed

+515
-68
lines changed

ecs-cli/modules/cli/compose/entity/service/service.go

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/context"
2222
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/entity"
2323
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/compose/entity/types"
24+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/cli/servicediscovery"
2425
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
2526
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils"
2627
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/cache"
@@ -35,21 +36,25 @@ import (
3536
// Service type is placeholder for a single task definition and its cache
3637
// and it performs operations on ECS Service level
3738
type Service struct {
38-
taskDef *ecs.TaskDefinition
39-
cache cache.Cache
40-
ecsContext *context.ECSContext
41-
timeSleeper *utils.TimeSleeper
42-
deploymentConfig *ecs.DeploymentConfiguration
43-
loadBalancer *ecs.LoadBalancer
44-
role string
45-
healthCheckGP *int64
39+
taskDef *ecs.TaskDefinition
40+
cache cache.Cache
41+
ecsContext *context.ECSContext
42+
timeSleeper *utils.TimeSleeper
43+
deploymentConfig *ecs.DeploymentConfiguration
44+
loadBalancer *ecs.LoadBalancer
45+
role string
46+
healthCheckGP *int64
47+
serviceRegistries []*ecs.ServiceRegistry
4648
}
4749

4850
const (
4951
ecsActiveResourceCode = "ACTIVE"
5052
ecsMissingResourceCode = "MISSING"
5153
)
5254

55+
// make servicediscovery.Create easily mockable in tests
56+
var servicediscoveryCreate = servicediscovery.Create
57+
5358
// NewService creates an instance of a Service and also sets up a cache for task definition
5459
func NewService(ecsContext *context.ECSContext) entity.ProjectEntity {
5560
return &Service{
@@ -402,6 +407,10 @@ func (s *Service) buildCreateServiceInput(serviceName, taskDefName string) (*ecs
402407
createServiceInput.HealthCheckGracePeriodSeconds = aws.Int64(*s.healthCheckGP)
403408
}
404409

410+
if len(s.serviceRegistries) > 0 {
411+
createServiceInput.ServiceRegistries = s.serviceRegistries
412+
}
413+
405414
if networkConfig != nil {
406415
createServiceInput.NetworkConfiguration = networkConfig
407416
}
@@ -448,6 +457,30 @@ func (s *Service) createService() error {
448457
serviceName := entity.GetServiceName(s)
449458
taskDefName := entity.GetIdFromArn(s.TaskDefinition().TaskDefinitionArn)
450459

460+
cliContext := s.Context().CLIContext
461+
462+
if cliContext.Bool(flags.EnableServiceDiscoveryFlag) {
463+
networkMode := aws.StringValue(s.TaskDefinition().NetworkMode)
464+
containerName := aws.String(cliContext.String(flags.ServiceDiscoveryContainerNameFlag))
465+
containerPort, err := getInt64FromCLIContext(s.Context(), flags.ServiceDiscoveryContainerPortFlag)
466+
if err != nil {
467+
return err
468+
}
469+
470+
serviceRegistryARN, err := servicediscoveryCreate(s.Context().CLIContext, networkMode, serviceName, s.Context().CommandConfig.Cluster)
471+
if err != nil {
472+
return err
473+
}
474+
475+
s.serviceRegistries = []*ecs.ServiceRegistry{
476+
&ecs.ServiceRegistry{
477+
RegistryArn: serviceRegistryARN,
478+
ContainerName: containerName,
479+
ContainerPort: containerPort,
480+
},
481+
}
482+
}
483+
451484
// Create request input
452485
createServiceInput, err := s.buildCreateServiceInput(serviceName, taskDefName)
453486
if err != nil {

ecs-cli/modules/cli/compose/entity/service/service_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,57 @@ func TestUpdateExistingServiceWithDesiredCountOverOne(t *testing.T) {
659659
upServiceWithNewTaskDefTest(t, cliContext, &config.CommandConfig{}, &utils.ECSParams{}, expectedInput, existingService)
660660
}
661661

662+
func TestCreateWithServiceDiscovery(t *testing.T) {
663+
sdsARN := "arn:aws:servicediscovery:eu-west-1:11111111111:service/srv-clydelovespudding"
664+
665+
flagSet := flag.NewFlagSet("ecs-cli-up", 0)
666+
flagSet.Bool(flags.EnableServiceDiscoveryFlag, true, "")
667+
668+
servicediscoveryCreate = func(c *cli.Context, networkMode, serviceName, clusterName string) (*string, error) {
669+
return aws.String(sdsARN), nil
670+
}
671+
672+
createServiceTest(
673+
t,
674+
flagSet,
675+
&config.CommandConfig{},
676+
&utils.ECSParams{},
677+
func(input *ecs.CreateServiceInput) {
678+
actualServiceRegistries := input.ServiceRegistries
679+
assert.Len(t, actualServiceRegistries, 1, "Expected a single Service Registry")
680+
assert.Equal(t, sdsARN, aws.StringValue(actualServiceRegistries[0].RegistryArn), "Service Registry should match")
681+
},
682+
)
683+
}
684+
685+
func TestCreateWithServiceDiscoveryWithContainerNameAndPort(t *testing.T) {
686+
sdsARN := "arn:aws:servicediscovery:eu-west-1:11111111111:service/srv-clydelovespudding"
687+
containerName := "nginx"
688+
689+
flagSet := flag.NewFlagSet("ecs-cli-up", 0)
690+
flagSet.Bool(flags.EnableServiceDiscoveryFlag, true, "")
691+
flagSet.String(flags.ServiceDiscoveryContainerNameFlag, containerName, "")
692+
flagSet.String(flags.ServiceDiscoveryContainerPortFlag, "80", "")
693+
694+
servicediscoveryCreate = func(c *cli.Context, networkMode, serviceName, clusterName string) (*string, error) {
695+
return aws.String(sdsARN), nil
696+
}
697+
698+
createServiceTest(
699+
t,
700+
flagSet,
701+
&config.CommandConfig{},
702+
&utils.ECSParams{},
703+
func(input *ecs.CreateServiceInput) {
704+
actualServiceRegistries := input.ServiceRegistries
705+
assert.Len(t, actualServiceRegistries, 1, "Expected a single Service Registry")
706+
assert.Equal(t, int64(80), aws.Int64Value(actualServiceRegistries[0].ContainerPort), "Expected container port to be 80")
707+
assert.Equal(t, containerName, aws.StringValue(actualServiceRegistries[0].ContainerName), "Expected ContainerName to match")
708+
assert.Equal(t, sdsARN, aws.StringValue(actualServiceRegistries[0].RegistryArn), "Service Registry should match")
709+
},
710+
)
711+
}
712+
662713
func getDefaultUpdateInput() UpdateServiceParams {
663714
return UpdateServiceParams{
664715
deploymentConfig: &ecs.DeploymentConfiguration{},
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Copyright 2015-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package servicediscovery
15+
16+
import (
17+
"fmt"
18+
19+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/cloudformation"
20+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags"
21+
"github.com/aws/amazon-ecs-cli/ecs-cli/modules/config"
22+
"github.com/aws/aws-sdk-go/aws"
23+
"github.com/aws/aws-sdk-go/service/ecs"
24+
"github.com/sirupsen/logrus"
25+
"github.com/urfave/cli"
26+
)
27+
28+
const (
29+
privateDNSNamespaceStackNameFormat = "amazon-ecs-cli-setup-%s-%s-private-dns-namespace"
30+
serviceDiscoveryServiceStackNameFormat = "amazon-ecs-cli-setup-%s-%s-service-discovery-service"
31+
)
32+
33+
// CloudFormation template parameters
34+
const (
35+
parameterKeyNamespaceDescription = "NamespaceDescription"
36+
parameterKeyVPCID = "VPCID"
37+
parameterKeyNamespaceName = "NamespaceName"
38+
)
39+
40+
const (
41+
parameterKeySDSDescription = "SDSDescription"
42+
parameterKeySDSName = "SDSName"
43+
parameterKeyNamespaceID = "NamespaceID"
44+
parameterKeyDNSType = "DNSType"
45+
parameterKeyDNSTTL = "DNSTTL"
46+
parameterKeyHealthCheckCustomConfigFailureThreshold = "FailureThreshold"
47+
)
48+
49+
const (
50+
CFNTemplateOutputPrivateNamespaceID = "PrivateDNSNamespaceID"
51+
CFNTemplateOutputSDSARN = "ServiceDiscoveryServiceARN"
52+
)
53+
54+
const (
55+
dnsRecordTypeA = "A"
56+
dnsRecordTypeSRV = "SRV"
57+
)
58+
59+
var requiredParamsSDS = []string{parameterKeyNamespaceID, parameterKeySDSName, parameterKeyDNSType}
60+
var requiredParamsNamespace = []string{parameterKeyVPCID, parameterKeyNamespaceName}
61+
62+
// Create creates resources for service discovery and returns the ID of the Service Discovery Service
63+
func Create(c *cli.Context, networkMode, serviceName, clusterName string) (*string, error) {
64+
rdwr, err := config.NewReadWriter()
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
commandConfig, err := config.NewCommandConfig(c, rdwr)
70+
if err != nil {
71+
return nil, err
72+
}
73+
74+
cfnClient := cloudformation.NewCloudformationClient(commandConfig)
75+
76+
return create(c, networkMode, serviceName, clusterName, cfnClient)
77+
}
78+
79+
func create(c *cli.Context, networkMode, serviceName, clusterName string, cfnClient cloudformation.CloudformationClient) (*string, error) {
80+
// create namespace
81+
namespaceParams := namespaceCFNParams(c)
82+
if err := namespaceParams.Validate(); err != nil {
83+
return nil, err
84+
}
85+
86+
namespaceStackName := cfnStackName(privateDNSNamespaceStackNameFormat, clusterName, serviceName)
87+
if _, err := cfnClient.CreateStack(cloudformation.GetPrivateNamespaceTemplate(), namespaceStackName, false, namespaceParams); err != nil {
88+
return nil, err
89+
}
90+
91+
logrus.Info("Waiting for the private DNS namespace to be created...")
92+
// Wait for stack creation
93+
cfnClient.WaitUntilCreateComplete(namespaceStackName)
94+
95+
// Get the ID of the namespace we just created
96+
namespaceID, err := getOutputIDFromStack(cfnClient, namespaceStackName, CFNTemplateOutputPrivateNamespaceID)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
// create SDS
102+
sdsParams := sdsCFNParams(aws.StringValue(namespaceID), serviceName, networkMode)
103+
if err := sdsParams.Validate(); err != nil {
104+
return nil, err
105+
}
106+
107+
sdsStackName := cfnStackName(serviceDiscoveryServiceStackNameFormat, clusterName, serviceName)
108+
if _, err := cfnClient.CreateStack(cloudformation.GetSDSTemplate(), sdsStackName, false, sdsParams); err != nil {
109+
return nil, err
110+
}
111+
112+
logrus.Info("Waiting for the Service Discovery Service to be created...")
113+
// Wait for stack creation
114+
cfnClient.WaitUntilCreateComplete(sdsStackName)
115+
116+
// Return the ID of the SDS we just created
117+
return getOutputIDFromStack(cfnClient, sdsStackName, CFNTemplateOutputSDSARN)
118+
}
119+
120+
func getOutputIDFromStack(cfnClient cloudformation.CloudformationClient, stackName, outputKey string) (*string, error) {
121+
response, err := cfnClient.DescribeStacks(stackName)
122+
if err != nil {
123+
return nil, err
124+
}
125+
if len(response.Stacks) == 0 {
126+
return nil, fmt.Errorf("Could not find CloudFormation stack: %s", stackName)
127+
}
128+
129+
for _, output := range response.Stacks[0].Outputs {
130+
if aws.StringValue(output.OutputKey) == outputKey {
131+
return output.OutputValue, nil
132+
}
133+
}
134+
return nil, fmt.Errorf("Failed to find output %s in stack %s", outputKey, stackName)
135+
136+
}
137+
138+
func cfnStackName(stackName, cluster, service string) string {
139+
return fmt.Sprintf(stackName, cluster, service)
140+
}
141+
142+
func sdsCFNParams(namespaceID, sdsName, networkMode string) *cloudformation.CfnStackParams {
143+
cfnParams := cloudformation.NewCfnStackParams(requiredParamsSDS)
144+
145+
cfnParams.Add(parameterKeyNamespaceID, namespaceID)
146+
cfnParams.Add(parameterKeySDSName, sdsName)
147+
148+
dnsType := dnsRecordTypeSRV
149+
if networkMode == ecs.NetworkModeAwsvpc {
150+
dnsType = dnsRecordTypeA
151+
}
152+
cfnParams.Add(parameterKeyDNSType, dnsType)
153+
154+
return cfnParams
155+
}
156+
157+
func namespaceCFNParams(context *cli.Context) *cloudformation.CfnStackParams {
158+
cfnParams := cloudformation.NewCfnStackParams(requiredParamsNamespace)
159+
160+
namespaceName := context.String(flags.PrivateDNSNamespaceNameFlag)
161+
cfnParams.Add(parameterKeyNamespaceName, namespaceName)
162+
163+
vpcID := context.String(flags.VpcIdFlag)
164+
cfnParams.Add(parameterKeyVPCID, vpcID)
165+
166+
return cfnParams
167+
}

0 commit comments

Comments
 (0)