// Copyright 2018 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 servicediscovery import ( "flag" "fmt" "strings" "testing" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/cloudformation" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/clients/aws/cloudformation/mock" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/commands/flags" "github.com/aws/amazon-ecs-cli/ecs-cli/modules/config" utils "github.com/aws/amazon-ecs-cli/ecs-cli/modules/utils/compose" "github.com/aws/aws-sdk-go/aws" sdk "github.com/aws/aws-sdk-go/service/cloudformation" "github.com/aws/aws-sdk-go/service/ecs" "github.com/aws/aws-sdk-go/service/servicediscovery" "github.com/golang/mock/gomock" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/urfave/cli" ) const ( testClusterName = "cluster" testServiceName = "service" nonDefaultSDSName = "some-custom-name" testDescription = "clyde loves pudding" otherTestDescription = "pudding plays hard to get" testNamespaceStackName = "amazon-ecs-cli-setup-private-dns-namespace-cluster-service" testSDSStackName = "amazon-ecs-cli-setup-service-discovery-service-cluster-service" testNamespaceName = "corp" otherTestNamespaceName = "dumpling" testVPCID = "vpc-8BAADF00D" otherTestVPCID = "vpc-F33DFAC3" testNamespaceID = "ns-CA15CA15CA15CA15" otherTestNamespaceID = "ns-D0G5D0G5D0G5D0G5D0G5" testPublicNamespaceID = "ns-PUBL1C" testSDSARN = "arn:aws:servicediscovery:eu-west-1:11111111111:service/srv-clydelovespudding" testContainerName = "my-container" otherTestContainerName = "my-other-container" ) type validateSDSParamsFunc func(*testing.T, *cloudformation.CfnStackParams) type validateNamespaceParamsFunc func(*testing.T, *cloudformation.CfnStackParams) func TestCreateServiceDiscoveryAWSVPC(t *testing.T) { var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(testVPCID, parameterKeyVPCID, cfnParams, t) } var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeA, parameterKeyDNSType, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does pre-exist return nil, nil } registry, err := testCreateServiceDiscovery(t, "awsvpc", &utils.ServiceDiscovery{}, simpleWorkflowContext(), validateNamespace, validateSDS, true) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") } func TestCreateServiceDiscoveryBridge(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(testVPCID, parameterKeyVPCID, cfnParams, t) } var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does not pre-exist return nil, nil } registry, err := testCreateServiceDiscovery(t, "bridge", input, simpleWorkflowContext(), validateNamespace, validateSDS, true) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryHost(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(testVPCID, parameterKeyVPCID, cfnParams, t) } var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does not pre-exist return nil, nil } registry, err := testCreateServiceDiscovery(t, "host", input, simpleWorkflowContext(), validateNamespace, validateSDS, true) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryForceRecreate(t *testing.T) { oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does pre-exist return nil, nil } ctrl := gomock.NewController(t) defer ctrl.Finish() describeNamespaceStackResponse := &sdk.DescribeStacksOutput{ Stacks: []*sdk.Stack{ &sdk.Stack{ Outputs: []*sdk.Output{ &sdk.Output{ OutputKey: aws.String(cfnTemplateOutputPrivateNamespaceID), OutputValue: aws.String(testNamespaceID), }, }, }, }, } describeSDSStackResponse := &sdk.DescribeStacksOutput{ Stacks: []*sdk.Stack{ &sdk.Stack{ Outputs: []*sdk.Output{ &sdk.Output{ OutputKey: aws.String(cfnTemplateOutputSDSARN), OutputValue: aws.String(testSDSARN), }, }, }, }, } mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testNamespaceStackName).Return(nil), // validate that existing SDS stack is deleted mockCloudformation.EXPECT().DeleteStack(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().CreateStack(gomock.Any(), testNamespaceStackName, false, gomock.Any(), gomock.Any()).Do(func(v, w, x, y, z interface{}) { stackName := w.(string) capabilityIAM := x.(bool) cfnParams := y.(*cloudformation.CfnStackParams) validateCFNParam(testNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(testVPCID, parameterKeyVPCID, cfnParams, t) assert.False(t, capabilityIAM, "Expected capability capabilityIAM to be false") assert.Equal(t, testNamespaceStackName, stackName, "Expected stack name to match") }).Return("", nil), mockCloudformation.EXPECT().WaitUntilCreateComplete(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().DescribeStacks(testNamespaceStackName).Return(describeNamespaceStackResponse, nil), mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(nil), // Validate that existing Namespace stack is deleted mockCloudformation.EXPECT().DeleteStack(testSDSStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testSDSStackName).Return(nil), mockCloudformation.EXPECT().CreateStack(gomock.Any(), testSDSStackName, false, gomock.Any(), gomock.Any()).Do(func(v, w, x, y, z interface{}) { stackName := w.(string) capabilityIAM := x.(bool) cfnParams := y.(*cloudformation.CfnStackParams) validateCFNParam(testNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeA, parameterKeyDNSType, cfnParams, t) assert.False(t, capabilityIAM, "Expected capability capabilityIAM to be false") assert.Equal(t, testSDSStackName, stackName, "Expected stack name to match") }).Return("", nil), mockCloudformation.EXPECT().WaitUntilCreateComplete(testSDSStackName).Return(nil), mockCloudformation.EXPECT().DescribeStacks(testSDSStackName).Return(describeSDSStackResponse, nil), ) config := &config.CommandConfig{ Cluster: testClusterName, } registry, err := create(simpleWorkflowContext(), "awsvpc", testServiceName, mockCloudformation, &utils.ServiceDiscovery{}, config) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") } func TestCreateServiceDiscoveryWithECSParams(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), PrivateDNSNamespace: utils.PrivateDNSNamespace{ Namespace: utils.Namespace{ Name: testNamespaceName, }, VPC: testVPCID, Description: testDescription, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: nonDefaultSDSName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeSrv, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(testVPCID, parameterKeyVPCID, cfnParams, t) validateCFNParam(testDescription, parameterKeyNamespaceDescription, cfnParams, t) } var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(nonDefaultSDSName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam("60", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does not pre-exist return nil, nil } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, emptyContext(), validateNamespace, validateSDS, true) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryWithECSParamsOverriddenByFlags(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), PrivateDNSNamespace: utils.PrivateDNSNamespace{ Namespace: utils.Namespace{ Name: testNamespaceName, }, VPC: testVPCID, Description: testDescription, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: testServiceName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeSrv, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } flagDNSTTL := "120" flagContPort := "22" flagHealthThreshold := "3" flagSet := flag.NewFlagSet("create-sd", 0) flagSet.String(flags.PrivateDNSNamespaceNameFlag, otherTestNamespaceName, "") flagSet.String(flags.VpcIdFlag, otherTestVPCID, "") flagSet.String(flags.DNSTTLFlag, flagDNSTTL, "") flagSet.String(flags.DNSTypeFlag, servicediscovery.RecordTypeA, "") flagSet.String(flags.ServiceDiscoveryContainerNameFlag, otherTestContainerName, "") flagSet.String(flags.ServiceDiscoveryContainerPortFlag, flagContPort, "") flagSet.String(flags.HealthcheckCustomConfigFailureThresholdFlag, flagHealthThreshold, "") flagSet.Bool(flags.ForceFlag, true, "") overrides := cli.NewContext(nil, flagSet, nil) var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(otherTestNamespaceName, parameterKeyNamespaceName, cfnParams, t) validateCFNParam(otherTestVPCID, parameterKeyVPCID, cfnParams, t) validateCFNParam(testDescription, parameterKeyNamespaceDescription, cfnParams, t) } var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeA, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam(flagDNSTTL, parameterKeyDNSTTL, cfnParams, t) validateCFNParam(flagHealthThreshold, parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does not pre-exist return nil, nil } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, overrides, validateNamespace, validateSDS, true) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, otherTestContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(22), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryWithECSParamsExistingPrivateNamespaceByID(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), PrivateDNSNamespace: utils.PrivateDNSNamespace{ Namespace: utils.Namespace{ ID: otherTestNamespaceID, }, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: testServiceName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeSrv, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) {} var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(otherTestNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam("60", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, emptyContext(), validateNamespace, validateSDS, false) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryWithECSParamsExistingPrivateNamespaceByName(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), PrivateDNSNamespace: utils.PrivateDNSNamespace{ Namespace: utils.Namespace{ Name: otherTestNamespaceName, }, VPC: otherTestVPCID, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: testServiceName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeSrv, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) {} var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(otherTestNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam("60", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } oldFindPrivateNamespace := findPrivateNamespace defer func() { findPrivateNamespace = oldFindPrivateNamespace }() findPrivateNamespace = func(name, vpc string, config *config.CommandConfig) (*string, error) { // In this test the namespace does pre-exist and we search for it return aws.String(otherTestNamespaceID), nil } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, emptyContext(), validateNamespace, validateSDS, false) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryWithECSParamsExistingPublicNamespaceByID(t *testing.T) { input := &utils.ServiceDiscovery{ ContainerName: testContainerName, ContainerPort: aws.Int64(80), PublicDNSNamespace: utils.PublicDNSNamespace{ Namespace: utils.Namespace{ ID: otherTestNamespaceID, }, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: testServiceName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeSrv, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) {} var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(otherTestNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeSrv, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam("60", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, emptyContext(), validateNamespace, validateSDS, false) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") assert.Equal(t, testContainerName, aws.StringValue(registry.ContainerName), "Expected container name to match") assert.Equal(t, int64(80), aws.Int64Value(registry.ContainerPort), "Expected container port to match") } func TestCreateServiceDiscoveryWithECSParamsExistingPublicNamespaceByName(t *testing.T) { input := &utils.ServiceDiscovery{ PublicDNSNamespace: utils.PublicDNSNamespace{ Namespace: utils.Namespace{ Name: otherTestNamespaceName, }, }, ServiceDiscoveryService: utils.ServiceDiscoveryService{ Name: testServiceName, Description: testDescription, DNSConfig: utils.DNSConfig{ TTL: aws.Int64(60), Type: servicediscovery.RecordTypeA, }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } var validateNamespace validateNamespaceParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) {} var validateSDS validateSDSParamsFunc = func(t *testing.T, cfnParams *cloudformation.CfnStackParams) { validateCFNParam(otherTestNamespaceID, parameterKeyNamespaceID, cfnParams, t) validateCFNParam(testServiceName, parameterKeySDSName, cfnParams, t) validateCFNParam(servicediscovery.RecordTypeA, parameterKeyDNSType, cfnParams, t) validateCFNParam(testDescription, parameterKeySDSDescription, cfnParams, t) validateCFNParam("60", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) } oldFindPublicNamespace := findPublicNamespace defer func() { findPublicNamespace = oldFindPublicNamespace }() findPublicNamespace = func(name string, config *config.CommandConfig) (*string, error) { // In this test the namespace does pre-exist and we search for it return aws.String(otherTestNamespaceID), nil } registry, err := testCreateServiceDiscovery(t, "awsvpc", input, emptyContext(), validateNamespace, validateSDS, false) assert.NoError(t, err, "Unexpected Error calling create") assert.Equal(t, testSDSARN, aws.StringValue(registry.RegistryArn), "Expected SDS ARN to match") logrus.Info(aws.StringValue(registry.ContainerName)) logrus.Info(aws.StringValue(registry.ContainerName) == "") assert.Nil(t, registry.ContainerName, "Expected container name to be nil") assert.Nil(t, registry.ContainerPort, "Expected container port to be nil") } func TestUpdateServiceDiscovery(t *testing.T) { input := &utils.ServiceDiscovery{ ServiceDiscoveryService: utils.ServiceDiscoveryService{ DNSConfig: utils.DNSConfig{ TTL: aws.Int64(120), }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } existingParameters := []*sdk.Parameter{ &sdk.Parameter{ ParameterKey: aws.String(parameterKeySDSDescription), ParameterValue: aws.String(testDescription), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeySDSName), ParameterValue: aws.String(testServiceName), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyNamespaceID), ParameterValue: aws.String(testNamespaceID), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyDNSType), ParameterValue: aws.String(servicediscovery.RecordTypeA), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyDNSTTL), ParameterValue: aws.String("60"), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyHealthCheckCustomConfigFailureThreshold), ParameterValue: aws.String("1"), }, } ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().GetStackParameters(testSDSStackName).Return(existingParameters, nil), mockCloudformation.EXPECT().UpdateStack(testSDSStackName, gomock.Any()).Do(func(x, y interface{}) { cfnParams := y.(*cloudformation.CfnStackParams) validateCFNParam("120", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) validateUsePreviousValueSet(parameterKeyDNSType, cfnParams, t) validateUsePreviousValueSet(parameterKeySDSDescription, cfnParams, t) validateUsePreviousValueSet(parameterKeySDSName, cfnParams, t) validateUsePreviousValueSet(parameterKeyNamespaceID, cfnParams, t) }).Return("", nil), mockCloudformation.EXPECT().WaitUntilUpdateComplete(testSDSStackName).Return(nil), ) err := update(emptyContext(), "awsvpc", testServiceName, testClusterName, mockCloudformation, input) assert.NoError(t, err, "Unexpected error calling update") } func TestUpdateServiceDiscoveryGetStackParametersError(t *testing.T) { input := &utils.ServiceDiscovery{ ServiceDiscoveryService: utils.ServiceDiscoveryService{ DNSConfig: utils.DNSConfig{ TTL: aws.Int64(120), }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().GetStackParameters(testSDSStackName).Return(nil, fmt.Errorf("Stack not found")), ) err := update(emptyContext(), "awsvpc", testServiceName, testClusterName, mockCloudformation, input) assert.Error(t, err, "Expected error calling update") } func TestUpdateServiceDiscoveryUpdateStackError(t *testing.T) { input := &utils.ServiceDiscovery{ ServiceDiscoveryService: utils.ServiceDiscoveryService{ DNSConfig: utils.DNSConfig{ TTL: aws.Int64(120), }, HealthCheckCustomConfig: utils.HealthCheckCustomConfig{ FailureThreshold: aws.Int64(2), }, }, } existingParameters := []*sdk.Parameter{ &sdk.Parameter{ ParameterKey: aws.String(parameterKeySDSDescription), ParameterValue: aws.String(testDescription), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeySDSName), ParameterValue: aws.String(testServiceName), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyNamespaceID), ParameterValue: aws.String(testNamespaceID), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyDNSType), ParameterValue: aws.String(servicediscovery.RecordTypeSrv), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyDNSTTL), ParameterValue: aws.String("60"), }, &sdk.Parameter{ ParameterKey: aws.String(parameterKeyHealthCheckCustomConfigFailureThreshold), ParameterValue: aws.String("1"), }, } ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().GetStackParameters(testSDSStackName).Return(existingParameters, nil), mockCloudformation.EXPECT().UpdateStack(testSDSStackName, gomock.Any()).Do(func(x, y interface{}) { cfnParams := y.(*cloudformation.CfnStackParams) validateCFNParam("120", parameterKeyDNSTTL, cfnParams, t) validateCFNParam("2", parameterKeyHealthCheckCustomConfigFailureThreshold, cfnParams, t) validateUsePreviousValueSet(parameterKeyDNSType, cfnParams, t) validateUsePreviousValueSet(parameterKeySDSDescription, cfnParams, t) validateUsePreviousValueSet(parameterKeySDSName, cfnParams, t) validateUsePreviousValueSet(parameterKeyNamespaceID, cfnParams, t) }).Return("", fmt.Errorf("Some error")), ) err := update(emptyContext(), "host", testServiceName, testClusterName, mockCloudformation, input) assert.Error(t, err, "Expected error calling update") } func TestDeleteServiceDiscovery(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(nil), mockCloudformation.EXPECT().DeleteStack(testSDSStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testSDSStackName).Return(nil), ) err := delete(emptyContext(), mockCloudformation, testServiceName, testServiceName, testClusterName) assert.NoError(t, err, "Unexpected error calling delete") } func TestDeleteServiceDiscoveryDeleteNamespace(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(nil), mockCloudformation.EXPECT().DeleteStack(testSDSStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testSDSStackName).Return(nil), mockCloudformation.EXPECT().ValidateStackExists(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().DeleteStack(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testNamespaceStackName).Return(nil), ) flagSet := flag.NewFlagSet("create-sd", 0) flagSet.Bool(flags.DeletePrivateNamespaceFlag, true, "") context := cli.NewContext(nil, flagSet, nil) err := delete(context, mockCloudformation, testServiceName, testServiceName, testClusterName) assert.NoError(t, err, "Unexpected error calling delete") } func TestDeleteServiceDiscoveryStackNotFoundErrorForSDS(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(fmt.Errorf("Stack not found")), ) // If no stack is found, then there is nothing to delete, so no error is returned err := delete(emptyContext(), mockCloudformation, testServiceName, testServiceName, testClusterName) assert.NoError(t, err, "Expected error calling delete") } func TestDeleteServiceDiscoveryStackNotFoundErrorForNamespaceWithDeleteNamespaceFlag(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(nil), mockCloudformation.EXPECT().DeleteStack(testSDSStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testSDSStackName).Return(nil), mockCloudformation.EXPECT().ValidateStackExists(testNamespaceStackName).Return(fmt.Errorf("Stack not found")), ) flagSet := flag.NewFlagSet("create-sd", 0) flagSet.Bool(flags.DeletePrivateNamespaceFlag, true, "") context := cli.NewContext(nil, flagSet, nil) err := delete(context, mockCloudformation, testServiceName, testServiceName, testClusterName) // Since the user requested us to delete their namespace, if we failed to delete it, then that's an error case assert.Error(t, err, "Expected error calling delete") } func TestCFNStackName(t *testing.T) { // underscore is allowed in cluster and service names, but not CFNStack names clusterName := "supercalifragilisticexpialidocious_________1234_" serviceName := "anotherreallylongstring_______________________________________hi______________________________________________________wassup__________________________________________123456789" sdsStackName := cfnStackName(serviceDiscoveryServiceStackNameFormat, clusterName, serviceName) namespaceStackName := cfnStackName(privateDNSNamespaceStackNameFormat, clusterName, serviceName) // underscore is allowed in cluster and service names, but not CFNStack names assert.False(t, strings.Contains(sdsStackName, "_"), "Underscores are not allowed in CFN Stack names") assert.False(t, strings.Contains(namespaceStackName, "_"), "Underscores are not allowed in CFN Stack names") // CFN Stacknames must be no longer than 128 characters assert.True(t, len(sdsStackName) <= 128, "CFN Stack names must be no longer than 128 characters") assert.True(t, len(namespaceStackName) <= 128, "CFN Stack names must be no longer than 128 characters") } // Tests the following weird/rare case which is technically allowed: // SDS wasn't create by CLI, so no SDS stack exists, But // Namespace was created by CLI, and user wants us to remove it. func TestDeleteServiceDiscoveryStackNotFoundErrorForSDSWithDeleteNamespaceFlag(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) gomock.InOrder( mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(fmt.Errorf("Stack not found")), mockCloudformation.EXPECT().ValidateStackExists(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().DeleteStack(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().WaitUntilDeleteComplete(testNamespaceStackName).Return(nil), ) flagSet := flag.NewFlagSet("create-sd", 0) flagSet.Bool(flags.DeletePrivateNamespaceFlag, true, "") context := cli.NewContext(nil, flagSet, nil) err := delete(context, mockCloudformation, testServiceName, testServiceName, testClusterName) assert.NoError(t, err, "Unexpected error calling delete") } func testCreateServiceDiscovery(t *testing.T, networkMode string, ecsParamsSD *utils.ServiceDiscovery, c *cli.Context, validateNamespace validateNamespaceParamsFunc, validateSDS validateSDSParamsFunc, createNamespace bool) (*ecs.ServiceRegistry, error) { ctrl := gomock.NewController(t) defer ctrl.Finish() describeNamespaceStackResponse := &sdk.DescribeStacksOutput{ Stacks: []*sdk.Stack{ &sdk.Stack{ Outputs: []*sdk.Output{ &sdk.Output{ OutputKey: aws.String(cfnTemplateOutputPrivateNamespaceID), OutputValue: aws.String(testNamespaceID), }, }, }, }, } describeSDSStackResponse := &sdk.DescribeStacksOutput{ Stacks: []*sdk.Stack{ &sdk.Stack{ Outputs: []*sdk.Output{ &sdk.Output{ OutputKey: aws.String(cfnTemplateOutputSDSARN), OutputValue: aws.String(testSDSARN), }, }, }, }, } mockCloudformation := mock_cloudformation.NewMockCloudformationClient(ctrl) // Create a list of expected calls for gomock.InOrder // The expected calls differ depending upon whether or not we are going to create a namespace or not var expectedCFNCalls []*gomock.Call if createNamespace { expectedCFNCalls = append(expectedCFNCalls, []*gomock.Call{ mockCloudformation.EXPECT().ValidateStackExists(testNamespaceStackName).Return(fmt.Errorf("Stack Not Found")), mockCloudformation.EXPECT().CreateStack(gomock.Any(), testNamespaceStackName, false, gomock.Any(), gomock.Any()).Do(func(v, w, x, y, z interface{}) { stackName := w.(string) capabilityIAM := x.(bool) cfnParams := y.(*cloudformation.CfnStackParams) validateNamespace(t, cfnParams) assert.False(t, capabilityIAM, "Expected capability capabilityIAM to be false") assert.Equal(t, testNamespaceStackName, stackName, "Expected stack name to match") }).Return("", nil), mockCloudformation.EXPECT().WaitUntilCreateComplete(testNamespaceStackName).Return(nil), mockCloudformation.EXPECT().DescribeStacks(testNamespaceStackName).Return(describeNamespaceStackResponse, nil), }...) } expectedCFNCalls = append(expectedCFNCalls, []*gomock.Call{ mockCloudformation.EXPECT().ValidateStackExists(testSDSStackName).Return(fmt.Errorf("Stack Not Found")), mockCloudformation.EXPECT().CreateStack(gomock.Any(), testSDSStackName, false, gomock.Any(), gomock.Any()).Do(func(v, w, x, y, z interface{}) { stackName := w.(string) capabilityIAM := x.(bool) cfnParams := y.(*cloudformation.CfnStackParams) validateSDS(t, cfnParams) assert.False(t, capabilityIAM, "Expected capability capabilityIAM to be false") assert.Equal(t, testSDSStackName, stackName, "Expected stack name to match") }).Return("", nil), mockCloudformation.EXPECT().WaitUntilCreateComplete(testSDSStackName).Return(nil), mockCloudformation.EXPECT().DescribeStacks(testSDSStackName).Return(describeSDSStackResponse, nil), }...) gomock.InOrder(expectedCFNCalls...) config := &config.CommandConfig{ Cluster: testClusterName, } return create(c, networkMode, testServiceName, mockCloudformation, ecsParamsSD, config) } func emptyContext() *cli.Context { flagSet := flag.NewFlagSet("create-sd", 0) return cli.NewContext(nil, flagSet, nil) } func simpleWorkflowContext() *cli.Context { flagSet := flag.NewFlagSet("create-sd", 0) flagSet.String(flags.PrivateDNSNamespaceNameFlag, testNamespaceName, "") flagSet.String(flags.VpcIdFlag, testVPCID, "") return cli.NewContext(nil, flagSet, nil) } func validateCFNParam(expectedValue, paramKey string, cfnParams *cloudformation.CfnStackParams, t *testing.T) { observedValue, err := cfnParams.GetParameter(paramKey) assert.NoError(t, err, "Unexpected error getting cfn parameter") assert.Equal(t, expectedValue, aws.StringValue(observedValue.ParameterValue), fmt.Sprintf("Expected %s to be %s", paramKey, expectedValue)) } func validateUsePreviousValueSet(paramKey string, cfnParams *cloudformation.CfnStackParams, t *testing.T) { observedValue, err := cfnParams.GetParameter(paramKey) assert.NoError(t, err, "Unexpected error getting cfn parameter") assert.True(t, aws.BoolValue(observedValue.UsePreviousValue), "Expected UsePreviousValue to be true") }