Skip to content

Modify CreateHandler to ensure backwards compatibility #188

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
4 changes: 3 additions & 1 deletion aws-ssm-parameter/.rpdk-config
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"artifact_type": "RESOURCE",
"typeName": "AWS::SSM::Parameter",
"language": "java",
"runtime": "java8",
Expand All @@ -12,5 +13,6 @@
"parameter"
],
"protocolVersion": "2.0.0"
}
},
"executableEntrypoint": "com.amazonaws.ssm.parameter.HandlerWrapperExecutable"
}
7 changes: 7 additions & 0 deletions aws-ssm-parameter/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Resources:
Principal:
Service: resources.cloudformation.amazonaws.com
Action: sts:AssumeRole
Condition:
StringEquals:
aws:SourceAccount:
Ref: AWS::AccountId
StringLike:
aws:SourceArn:
Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/AWS-SSM-Parameter/*
Path: "/"
Policies:
- PolicyName: ResourceTypePolicy
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.amazonaws.ssm.parameter;

import com.amazonaws.util.CollectionUtils;
import com.google.common.collect.ImmutableSet;
import org.apache.commons.lang3.builder.EqualsBuilder;
import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParametersResponse;
import software.amazon.awssdk.services.ssm.model.InternalServerErrorException;
import software.amazon.awssdk.services.ssm.model.Parameter;
import software.amazon.awssdk.services.ssm.model.PutParameterRequest;
import software.amazon.awssdk.services.ssm.model.PutParameterResponse;
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
Expand Down Expand Up @@ -55,6 +58,27 @@ protected Constant getBackOffDelay(final ResourceModel model) {
}
}

protected boolean preExistenceCheck(final ResourceModel resourceModel,
final ProxyClient<SsmClient> proxyClient) {
final GetParametersResponse getParametersResponse;
try {
getParametersResponse = proxyClient.injectCredentialsAndInvokeV2(Translator.getParametersRequest(resourceModel), proxyClient.client()::getParameters);
} catch (final InternalServerErrorException exception) {
return false;
}

Parameter parameter = Parameter.builder().build();
if (!CollectionUtils.isNullOrEmpty(getParametersResponse.parameters())) {
parameter = getParametersResponse.parameters().get(0);
}
return new EqualsBuilder()
.append(resourceModel.getName(), parameter.name())
.append(resourceModel.getType(), parameter.typeAsString())
.append(resourceModel.getValue(), parameter.value())
.build();
}


/**
* If your resource requires some form of stabilization (e.g. service does not provide strong
* consistency), you will need to ensure that your code accounts for any potential issues, so that
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.amazonaws.ssm.parameter;

import com.amazonaws.AmazonServiceException;

import org.apache.commons.lang3.RandomStringUtils;

import software.amazon.awssdk.services.ssm.SsmClient;
Expand Down Expand Up @@ -54,6 +55,13 @@ protected ProgressEvent<ResourceModel, CallbackContext> handleRequest(
HandlerErrorCode.InvalidRequest);
}

// To maintain backwards compatibility, we are intentionally breaking uluru contract to return AlreadyExists exception if the
// resource already existed before the create request, https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html
if(preExistenceCheck(model, proxyClient)) {
logger.log("Parameter already exists with the same configuration, returning success");
return ProgressEvent.defaultSuccessHandler(model);
}

Map<String, String> consolidatedTagList = new HashMap<>();
if (request.getDesiredResourceTags() != null) {
consolidatedTagList.putAll(request.getDesiredResourceTags());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
package com.amazonaws.ssm.parameter;

import com.amazonaws.AmazonServiceException;

import org.apache.commons.lang3.RandomStringUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.Mock;
import org.mockito.Mockito;

import java.time.Duration;

import software.amazon.awssdk.services.ssm.SsmClient;
import software.amazon.awssdk.services.ssm.model.GetParametersResponse;
import software.amazon.awssdk.services.ssm.model.GetParametersRequest;
Expand All @@ -23,19 +33,13 @@
import software.amazon.cloudformation.proxy.ResourceHandlerRequest;
import software.amazon.cloudformation.proxy.OperationStatus;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.time.Duration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verifyNoMoreInteractions;
Expand Down Expand Up @@ -100,6 +104,9 @@ public void handleRequest_SimpleSuccess() {
.systemTags(SYSTEM_TAGS_SET)
.desiredResourceState(RESOURCE_MODEL)
.logicalResourceIdentifier("logicalId").build();

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());
final ProgressEvent<ResourceModel, CallbackContext> response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger);

assertThat(response).isNotNull();
Expand All @@ -114,6 +121,39 @@ public void handleRequest_SimpleSuccess() {
verify(ssmClient, atLeastOnce()).serviceName();
}

@Test
public void handleRequest_WithParameterAlreadyExistingOutOfBandWithSameConfiguration() {
final GetParametersResponse getParametersResponse = GetParametersResponse.builder()
.parameters(Parameter.builder()
.name(NAME)
.type(TYPE_STRING)
.value(VALUE)
.version(VERSION).build())
.build();
when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse);

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
.systemTags(SYSTEM_TAGS_SET)
.desiredResourceState(RESOURCE_MODEL)
.logicalResourceIdentifier("logicalId").build();

final ProgressEvent<ResourceModel, CallbackContext> response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger);

assertThat(response).isNotNull();
assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS);
assertThat(response.getCallbackContext()).isNull();
assertThat(response.getCallbackDelaySeconds()).isEqualTo(0);
assertThat(response.getResourceModels()).isNull();
assertThat(response.getMessage()).isNull();
assertThat(response.getErrorCode()).isNull();

verify(proxySsmClient.client(), times(0)).putParameter(any(PutParameterRequest.class));
verify(proxySsmClient.client(), times(1)).getParameters(any(GetParametersRequest.class));
verify(ssmClient, never()).serviceName();
}

@Test
public void handleRequest_SecureStringFailure() {
RESOURCE_MODEL = ResourceModel.builder()
Expand Down Expand Up @@ -257,6 +297,9 @@ public void handleRequest_SimpleSuccess_WithImageDataType() {
.build();
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse);

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
Expand Down Expand Up @@ -286,6 +329,9 @@ public void handleRequest_AmazonServiceException400ThrottlingException() {
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class)))
.thenThrow(amazonServiceException);

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
Expand All @@ -307,6 +353,8 @@ public void handleRequest_AmazonServiceException400ThrottlingException() {
public void handleRequest_ParameterAlreadyExistsException() {
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class)))
.thenThrow(ParameterAlreadyExistsException.builder().build());
handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
Expand All @@ -333,6 +381,9 @@ public void handleRequest_AmazonServiceException500Exception() {
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class)))
.thenThrow(amazonServiceException);

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
Expand All @@ -359,6 +410,9 @@ public void handleRequest_AmazonServiceException400NonThrottlingException() {
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class)))
.thenThrow(amazonServiceException);

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
Expand All @@ -381,6 +435,9 @@ public void handleRequest_AmazonServiceExceptionInternalServerError() {
when(proxySsmClient.client().putParameter(any(PutParameterRequest.class)))
.thenThrow(InternalServerErrorException.builder().build());

handler = Mockito.spy(handler);
doReturn(Boolean.FALSE).when(handler).preExistenceCheck(any(), any());

final ResourceHandlerRequest<ResourceModel> request = ResourceHandlerRequest.<ResourceModel>builder()
.clientRequestToken("token")
.desiredResourceTags(TAG_SET)
Expand Down