From 6c54858ff37d29fa45903949dca8345ba0e739fe Mon Sep 17 00:00:00 2001 From: "Michael, James" Date: Thu, 1 Jun 2017 10:09:06 -0700 Subject: [PATCH] AMI tag check added for all operations and commands which handles AMI. --- .../com/nike/cerberus/ConfigConstants.java | 10 ++ .../command/cms/CreateCmsClusterCommand.java | 11 ++ .../consul/CreateConsulClusterCommand.java | 11 ++ .../command/core/UpdateStackCommand.java | 9 ++ .../gateway/CreateGatewayClusterCommand.java | 9 ++ .../vault/CreateVaultClusterCommand.java | 11 ++ .../cms/CreateCmsClusterOperation.java | 10 ++ .../consul/CreateConsulClusterOperation.java | 10 ++ .../operation/core/UpdateStackOperation.java | 13 +- .../CreateGatewayClusterOperation.java | 10 ++ .../vault/CreateVaultClusterOperation.java | 10 ++ .../cerberus/service/AmiTagCheckService.java | 88 ++++++++++++ .../service/AmiTagCheckServiceTest.java | 131 ++++++++++++++++++ 13 files changed, 332 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/nike/cerberus/service/AmiTagCheckService.java create mode 100644 src/test/java/com/nike/cerberus/service/AmiTagCheckServiceTest.java diff --git a/src/main/java/com/nike/cerberus/ConfigConstants.java b/src/main/java/com/nike/cerberus/ConfigConstants.java index 351ef411..e752d9a9 100644 --- a/src/main/java/com/nike/cerberus/ConfigConstants.java +++ b/src/main/java/com/nike/cerberus/ConfigConstants.java @@ -107,4 +107,14 @@ public class ConfigConstants { JDBC_URL_KEY, JDBC_USERNAME_KEY, JDBC_PASSWORD_KEY); + + public static final String CERBERUS_AMI_TAG_NAME = "tag:cerberus_component"; + + public static final String CMS_AMI_TAG_VALUE = "cms"; + + public static final String GATEWAY_AMI_TAG_VALUE = "gateway"; + + public static final String CONSUL_AMI_TAG_VALUE = "consul"; + + public static final String VAULT_AMI_TAG_VALUE = "vault"; } \ No newline at end of file diff --git a/src/main/java/com/nike/cerberus/command/cms/CreateCmsClusterCommand.java b/src/main/java/com/nike/cerberus/command/cms/CreateCmsClusterCommand.java index 4fa0bca7..ace734f5 100644 --- a/src/main/java/com/nike/cerberus/command/cms/CreateCmsClusterCommand.java +++ b/src/main/java/com/nike/cerberus/command/cms/CreateCmsClusterCommand.java @@ -16,6 +16,7 @@ package com.nike.cerberus.command.cms; +import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; import com.nike.cerberus.command.Command; @@ -33,6 +34,8 @@ public class CreateCmsClusterCommand implements Command { public static final String COMMAND_NAME = "create-cms-cluster"; + public static final String SKIP_AMI_TAG_CHECK_ARG = "--skip-ami-tag-check"; + @ParametersDelegate private StackDelegate stackDelegate = new StackDelegate(); @@ -45,6 +48,14 @@ public String getCommandName() { return COMMAND_NAME; } + @Parameter(names = SKIP_AMI_TAG_CHECK_ARG, + description = "Flag for skipping validation of AMI with matching stackname tags") + private boolean skipAmiTagCheck; + + public boolean isSkipAmiTagCheck() { + return skipAmiTagCheck; + } + @Override public Class> getOperationClass() { return CreateCmsClusterOperation.class; diff --git a/src/main/java/com/nike/cerberus/command/consul/CreateConsulClusterCommand.java b/src/main/java/com/nike/cerberus/command/consul/CreateConsulClusterCommand.java index f1ebec80..e2a7b5f6 100644 --- a/src/main/java/com/nike/cerberus/command/consul/CreateConsulClusterCommand.java +++ b/src/main/java/com/nike/cerberus/command/consul/CreateConsulClusterCommand.java @@ -16,6 +16,7 @@ package com.nike.cerberus.command.consul; +import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; import com.nike.cerberus.command.Command; @@ -33,6 +34,8 @@ public class CreateConsulClusterCommand implements Command { public static final String COMMAND_NAME = "create-consul-cluster"; + public static final String SKIP_AMI_TAG_CHECK_ARG = "--skip-ami-tag-check"; + @ParametersDelegate private StackDelegate stackDelegate = new StackDelegate(); @@ -45,6 +48,14 @@ public String getCommandName() { return COMMAND_NAME; } + @Parameter(names = SKIP_AMI_TAG_CHECK_ARG, + description = "Flag for skipping validation of AMI with matching stackname tags") + private boolean skipAmiTagCheck; + + public boolean isSkipAmiTagCheck() { + return skipAmiTagCheck; + } + @Override public Class> getOperationClass() { return CreateConsulClusterOperation.class; diff --git a/src/main/java/com/nike/cerberus/command/core/UpdateStackCommand.java b/src/main/java/com/nike/cerberus/command/core/UpdateStackCommand.java index 60407af7..190d2ec2 100644 --- a/src/main/java/com/nike/cerberus/command/core/UpdateStackCommand.java +++ b/src/main/java/com/nike/cerberus/command/core/UpdateStackCommand.java @@ -39,6 +39,7 @@ public class UpdateStackCommand implements Command { public static final String COMMAND_NAME = "update-stack"; public static final String OVERWRITE_TEMPLATE_LONG_ARG = "--overwrite-template"; public static final String PARAMETER_SHORT_ARG = "-P"; + public static final String SKIP_AMI_TAG_CHECK_ARG = "--skip-ami-tag-check"; @Parameter(names = {"--stack-name"}, required = true, description = "The stack name to update.") private StackName stackName; @@ -78,6 +79,10 @@ public class UpdateStackCommand implements Command { @Parameter(names = StackDelegate.MIN_INSTANCES_LONG_ARG, description = "Minimum number of autos scaling instances") private Integer minimumInstances; + @Parameter(names = SKIP_AMI_TAG_CHECK_ARG, + description = "Flag for skipping validation of AMI with matching stackname tags") + private boolean skipAmiTagCheck; + @DynamicParameter(names = PARAMETER_SHORT_ARG, description = "Dynamic parameters for overriding the values for specific parameters in the CloudFormation.") private Map dynamicParameters = new HashMap<>(); @@ -129,6 +134,10 @@ public Integer getMinimumInstances() { return minimumInstances; } + public boolean isSkipAmiTagCheck() { + return skipAmiTagCheck; + } + @Override public String getCommandName() { return COMMAND_NAME; diff --git a/src/main/java/com/nike/cerberus/command/gateway/CreateGatewayClusterCommand.java b/src/main/java/com/nike/cerberus/command/gateway/CreateGatewayClusterCommand.java index 59b402ea..4ed12151 100644 --- a/src/main/java/com/nike/cerberus/command/gateway/CreateGatewayClusterCommand.java +++ b/src/main/java/com/nike/cerberus/command/gateway/CreateGatewayClusterCommand.java @@ -35,6 +35,7 @@ public class CreateGatewayClusterCommand implements Command { public static final String COMMAND_NAME = "create-gateway-cluster"; public static final String HOSTED_ZONE_ID_LONG_ARG = "--hosted-zone-id"; public static final String HOSTNAME_LONG_ARG = "--hostname"; + public static final String SKIP_AMI_TAG_CHECK_ARG = "--skip-ami-tag-check"; @Parameter(names = HOSTED_ZONE_ID_LONG_ARG, description = "The Route 53 hosted zone ID that will be used to create the CNAME record for Cerberus.", @@ -46,6 +47,10 @@ public class CreateGatewayClusterCommand implements Command { required = true) private String hostname; + @Parameter(names = SKIP_AMI_TAG_CHECK_ARG, + description = "Flag for skipping validation of AMI with matching stackname tags") + private boolean skipAmiTagCheck; + @ParametersDelegate private StackDelegate stackDelegate = new StackDelegate(); @@ -57,6 +62,10 @@ public String getHostname() { return hostname; } + public boolean isSkipAmiTagCheck() { + return skipAmiTagCheck; + } + public StackDelegate getStackDelegate() { return stackDelegate; } diff --git a/src/main/java/com/nike/cerberus/command/vault/CreateVaultClusterCommand.java b/src/main/java/com/nike/cerberus/command/vault/CreateVaultClusterCommand.java index 8ac31481..865a518f 100644 --- a/src/main/java/com/nike/cerberus/command/vault/CreateVaultClusterCommand.java +++ b/src/main/java/com/nike/cerberus/command/vault/CreateVaultClusterCommand.java @@ -16,6 +16,7 @@ package com.nike.cerberus.command.vault; +import com.beust.jcommander.Parameter; import com.beust.jcommander.Parameters; import com.beust.jcommander.ParametersDelegate; import com.nike.cerberus.command.Command; @@ -33,6 +34,8 @@ public class CreateVaultClusterCommand implements Command { public static final String COMMAND_NAME = "create-vault-cluster"; + public static final String SKIP_AMI_TAG_CHECK_ARG = "--skip-ami-tag-check"; + @ParametersDelegate private StackDelegate stackDelegate = new StackDelegate(); @@ -45,6 +48,14 @@ public String getCommandName() { return COMMAND_NAME; } + @Parameter(names = SKIP_AMI_TAG_CHECK_ARG, + description = "Flag for skipping validation of AMI with matching stackname tags") + private boolean skipAmiTagCheck; + + public boolean isSkipAmiTagCheck() { + return skipAmiTagCheck; + } + @Override public Class> getOperationClass() { return CreateVaultClusterOperation.class; diff --git a/src/main/java/com/nike/cerberus/operation/cms/CreateCmsClusterOperation.java b/src/main/java/com/nike/cerberus/operation/cms/CreateCmsClusterOperation.java index b3871861..7e341893 100644 --- a/src/main/java/com/nike/cerberus/operation/cms/CreateCmsClusterOperation.java +++ b/src/main/java/com/nike/cerberus/operation/cms/CreateCmsClusterOperation.java @@ -30,6 +30,7 @@ import com.nike.cerberus.operation.UnexpectedCloudFormationStatusException; import com.nike.cerberus.service.CloudFormationService; import com.nike.cerberus.service.Ec2UserDataService; +import com.nike.cerberus.service.AmiTagCheckService; import com.nike.cerberus.store.ConfigStore; import com.nike.cerberus.util.UuidSupplier; import org.slf4j.Logger; @@ -55,6 +56,8 @@ public class CreateCmsClusterOperation implements Operation { private final Ec2UserDataService ec2UserDataService; + private final AmiTagCheckService amiTagCheckService; + @Inject public UpdateStackOperation(final ConfigStore configStore, final CloudFormationService cloudFormationService, @Named(CF_OBJECT_MAPPER) final ObjectMapper cloudformationObjectMapper, - final Ec2UserDataService ec2UserDataService) { + final Ec2UserDataService ec2UserDataService, + final AmiTagCheckService amiTagCheckService) { this.configStore = configStore; this.cloudFormationService = cloudFormationService; this.cloudformationObjectMapper = cloudformationObjectMapper; this.ec2UserDataService = ec2UserDataService; + this.amiTagCheckService = amiTagCheckService; stackParameterMap = new HashMap<>(); stackParameterMap.put(StackName.CONSUL, ConsulParameters.class); @@ -109,6 +114,12 @@ public void run(final UpdateStackCommand command) { throw new IllegalArgumentException("The specified stack does not support the update stack command!"); } + // Make sure the given AmiId is for this component. Check if it contains required tag + // There is no AMI for Base. + if ( !command.isSkipAmiTagCheck() && StackName.BASE != command.getStackName() ) { + amiTagCheckService.validateAmiTagForStack(command.getAmiId(),command.getStackName()); + } + parameters.putAll(command.getDynamicParameters()); try { diff --git a/src/main/java/com/nike/cerberus/operation/gateway/CreateGatewayClusterOperation.java b/src/main/java/com/nike/cerberus/operation/gateway/CreateGatewayClusterOperation.java index 116e9ae1..3893bd9c 100644 --- a/src/main/java/com/nike/cerberus/operation/gateway/CreateGatewayClusterOperation.java +++ b/src/main/java/com/nike/cerberus/operation/gateway/CreateGatewayClusterOperation.java @@ -31,6 +31,7 @@ import com.nike.cerberus.operation.UnexpectedCloudFormationStatusException; import com.nike.cerberus.service.CloudFormationService; import com.nike.cerberus.service.Ec2UserDataService; +import com.nike.cerberus.service.AmiTagCheckService; import com.nike.cerberus.store.ConfigStore; import com.nike.cerberus.util.UuidSupplier; import org.apache.commons.lang3.StringUtils; @@ -57,6 +58,8 @@ public class CreateGatewayClusterOperation implements Operation stackAmiTagValueMap; + + @Inject + public AmiTagCheckService(final AmazonEC2 ec2Client) { + this.ec2Client = ec2Client; + + stackAmiTagValueMap = new HashMap<>(); + stackAmiTagValueMap.put(StackName.CONSUL, ConfigConstants.CONSUL_AMI_TAG_VALUE); + stackAmiTagValueMap.put(StackName.VAULT, ConfigConstants.VAULT_AMI_TAG_VALUE); + stackAmiTagValueMap.put(StackName.CMS, ConfigConstants.CMS_AMI_TAG_VALUE); + stackAmiTagValueMap.put(StackName.GATEWAY, ConfigConstants.GATEWAY_AMI_TAG_VALUE); + + } + + /** + * Validates if the given AMI has given tag and value. + * + * @return true if matches otherwise false + */ + public boolean isAmiWithTagExist(final String amiId, final String tagName, final String tagValue) { + + final DescribeImagesRequest request = new DescribeImagesRequest() + .withFilters(new Filter().withName(tagName).withValues(tagValue)) + .withFilters(new Filter().withName("image-id").withValues(amiId)); + + try { + final DescribeImagesResult result = ec2Client.describeImages(request); + return result.getImages().size() > 0; + } catch (final AmazonServiceException ase) { + if (ase.getErrorCode() == "InvalidAMIID.NotFound") { + return false; + } + + throw ase; + } + } + + /** + * Validates if the given AMI has given cerberus tag and value. + * + * @return void + */ + public void validateAmiTagForStack(final String amiId, final StackName stackName) { + if (!isAmiWithTagExist(amiId, ConfigConstants.CERBERUS_AMI_TAG_NAME, stackAmiTagValueMap.get(stackName) )) { + throw new IllegalStateException("AMI tag check failed!. Given AMI ID does not contain cerberus tag 'cerberus_component' with stack name"); + } + } +} diff --git a/src/test/java/com/nike/cerberus/service/AmiTagCheckServiceTest.java b/src/test/java/com/nike/cerberus/service/AmiTagCheckServiceTest.java new file mode 100644 index 00000000..10993704 --- /dev/null +++ b/src/test/java/com/nike/cerberus/service/AmiTagCheckServiceTest.java @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2016 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License 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 com.nike.cerberus.service; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.services.ec2.AmazonEC2; +import com.amazonaws.services.ec2.model.DescribeImagesRequest; +import com.amazonaws.services.ec2.model.DescribeImagesResult; +import com.amazonaws.services.ec2.model.Filter; +import com.amazonaws.services.ec2.model.Image; +import org.junit.Test; + +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class AmiTagCheckServiceTest { + + @Test + public void isAmiWithTagExistTrue() { + AmazonEC2 ec2Client = mock(AmazonEC2.class); + AmiTagCheckService amiTagCheckService = new AmiTagCheckService(ec2Client); + + String amiId = "ami-1234abcd"; + String tagName = "sometag"; + String tagValue = "someval"; + + when(ec2Client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter().withName(tagName).withValues(tagValue)) + .withFilters(new Filter().withName("image-id").withValues(amiId)) + ) + ).thenReturn( + new DescribeImagesResult().withImages(new Image()) + ); + + // invoke method under test + assertTrue(amiTagCheckService.isAmiWithTagExist(amiId, tagName, tagValue)); + } + + @Test + public void isAmiWithTagExistFalse() { + AmazonEC2 ec2Client = mock(AmazonEC2.class); + AmiTagCheckService amiTagCheckService = new AmiTagCheckService(ec2Client); + + String amiId = "ami-1234abcd"; + String tagName = "sometag"; + String tagValue = "someval"; + + when(ec2Client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter().withName(tagName).withValues(tagValue)) + .withFilters(new Filter().withName("image-id").withValues(amiId)) + ) + ).thenReturn( + new DescribeImagesResult() + ); + + // invoke method under test + assertFalse(amiTagCheckService.isAmiWithTagExist(amiId, tagName, tagValue)); + } + + @Test + public void isAmiWithTagExistNotFound() { + AmazonEC2 ec2Client = mock(AmazonEC2.class); + AmiTagCheckService amiTagCheckService = new AmiTagCheckService(ec2Client); + + String amiId = "ami-1234abcd"; + String tagName = "sometag"; + String tagValue = "someval"; + + AmazonServiceException ex = new AmazonServiceException("fake-exception"); + ex.setErrorCode("InvalidAMIID.NotFound"); + + when(ec2Client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter().withName(tagName).withValues(tagValue)) + .withFilters(new Filter().withName("image-id").withValues(amiId)) + ) + ).thenThrow(ex); + + // invoke method under test + assertFalse(amiTagCheckService.isAmiWithTagExist(amiId, tagName, tagValue)); + } + + @Test + public void isAmiWithTagExistThrowException() { + AmazonEC2 ec2Client = mock(AmazonEC2.class); + AmiTagCheckService amiTagCheckService = new AmiTagCheckService(ec2Client); + + String amiId = "ami-1234abcd"; + String tagName = "sometag"; + String tagValue = "someval"; + String unknownAwsExMessage = "Unknown AWS exception message"; + + when(ec2Client.describeImages( + new DescribeImagesRequest() + .withFilters(new Filter().withName(tagName).withValues(tagValue)) + .withFilters(new Filter().withName("image-id").withValues(amiId)) + ) + ).thenThrow(new AmazonServiceException(unknownAwsExMessage)); + + try { + // invoke method under test + amiTagCheckService.isAmiWithTagExist(amiId, tagName, tagValue); + fail("Expected exception message '" + unknownAwsExMessage + "'not received"); + } catch (AmazonServiceException ex) { + // pass + assertEquals(unknownAwsExMessage, ex.getErrorMessage()); + } + } +} \ No newline at end of file