Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 36e4e41

Browse files
authoredMar 21, 2025··
Add support for stack and change set level hooks
* Add StackHookTargetModel for support of stack-level hooks Add new type that represents the payload sent to hook handlers that will contain information related to stack hooks. * Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. * Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. * Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. * Skip stack level hook for stack if prior stack level change set hook succeeded For stack level hooks, customers are able to return a new status that allow stack level hooks that execute against a stack to skip with a successful status. The idea is that if a stack hook invoked against a change set succeeds, there is no need to invoke against the stack once the change set is processed. * Skip stack level hook for stack if prior stack level change set hook succeeded For stack level hooks, customers are able to return a new status that allow stack level hooks that execute against a stack to skip with a successful status. The idea is that if a stack hook invoked against a change set succeeds, there is no need to invoke against the stack once the change set is processed. * fix method * Fix resource targetting for a stack level hook * Fix resource targetting for a stack level hook * bump version
1 parent 3b91e11 commit 36e4e41

File tree

13 files changed

+441
-14
lines changed

13 files changed

+441
-14
lines changed
 

‎pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<modelVersion>4.0.0</modelVersion>
44
<groupId>software.amazon.cloudformation</groupId>
55
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
6-
<version>2.1.1</version>
6+
<version>2.2.1</version>
77
<name>AWS CloudFormation RPDK Java Plugin</name>
88
<description>The CloudFormation Resource Provider Development Kit (RPDK) allows you to author your own resource providers that can be used by CloudFormation. This plugin library helps to provide runtime bindings for the execution of your providers by CloudFormation.
99
</description>
@@ -491,7 +491,7 @@
491491
<plugin>
492492
<groupId>org.sonatype.plugins</groupId>
493493
<artifactId>nexus-staging-maven-plugin</artifactId>
494-
<version>1.6.13</version>
494+
<version>1.6.8</version>
495495
<extensions>true</extensions>
496496
<configuration>
497497
<serverId>sonatype-nexus-staging</serverId>

‎python/rpdk/java/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import logging
22

3-
__version__ = "2.1.1"
3+
__version__ = "2.2.1"
44

55
logging.getLogger(__name__).addHandler(logging.NullHandler())

‎src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717
import com.amazonaws.AmazonServiceException;
1818
import com.amazonaws.retry.RetryUtils;
1919
import com.fasterxml.jackson.core.type.TypeReference;
20+
import com.google.common.annotations.VisibleForTesting;
21+
import java.io.ByteArrayOutputStream;
2022
import java.io.IOException;
2123
import java.io.InputStream;
2224
import java.io.OutputStream;
25+
import java.net.URISyntaxException;
26+
import java.net.URL;
2327
import java.nio.charset.StandardCharsets;
2428
import java.time.Instant;
29+
import java.util.Collections;
2530
import java.util.Date;
31+
import java.util.Map;
2632
import org.apache.commons.io.FileUtils;
2733
import org.apache.commons.io.IOUtils;
2834
import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -31,10 +37,15 @@
3137
import org.slf4j.Logger;
3238
import org.slf4j.LoggerFactory;
3339
import software.amazon.awssdk.awscore.exception.AwsServiceException;
40+
import software.amazon.awssdk.http.HttpExecuteRequest;
41+
import software.amazon.awssdk.http.HttpExecuteResponse;
3442
import software.amazon.awssdk.http.HttpStatusCode;
3543
import software.amazon.awssdk.http.HttpStatusFamily;
3644
import software.amazon.awssdk.http.SdkHttpClient;
45+
import software.amazon.awssdk.http.SdkHttpMethod;
46+
import software.amazon.awssdk.http.SdkHttpRequest;
3747
import software.amazon.awssdk.http.apache.ApacheHttpClient;
48+
import software.amazon.awssdk.utils.IoUtils;
3849
import software.amazon.cloudformation.encryption.Cipher;
3950
import software.amazon.cloudformation.encryption.KMSCipher;
4051
import software.amazon.cloudformation.exceptions.BaseHandlerException;
@@ -63,6 +74,7 @@
6374
import software.amazon.cloudformation.proxy.hook.HookInvocationRequest;
6475
import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
6576
import software.amazon.cloudformation.proxy.hook.HookRequestContext;
77+
import software.amazon.cloudformation.proxy.hook.HookRequestData;
6678
import software.amazon.cloudformation.proxy.hook.HookStatus;
6779
import software.amazon.cloudformation.resource.SchemaValidator;
6880
import software.amazon.cloudformation.resource.Serializer;
@@ -89,6 +101,9 @@ public abstract class HookAbstractWrapper<TargetT, CallbackT, ConfigurationT> {
89101
final SchemaValidator validator;
90102
final TypeReference<HookInvocationRequest<ConfigurationT, CallbackT>> typeReference;
91103

104+
final TypeReference<Map<String, Object>> hookStackPayloadS3TypeReference = new TypeReference<>() {
105+
};
106+
92107
private MetricsPublisher providerMetricsPublisher;
93108

94109
private CloudWatchLogHelper cloudWatchLogHelper;
@@ -222,18 +237,20 @@ private ProgressEvent<TargetT, CallbackT> processInvocation(final JSONObject raw
222237

223238
assert request != null : "Invalid request object received. Request object is null";
224239

225-
if (request.getRequestData() == null || request.getRequestData().getTargetModel() == null) {
226-
throw new TerminalException("Invalid request object received. Target Model can not be null.");
227-
}
228-
229-
// TODO: Include hook schema validation here after schema is finalized
240+
boolean isPayloadRemote = isHookInvocationPayloadRemote(request.getRequestData());
230241

231242
try {
232243
// initialise dependencies with platform credentials
233244
initialiseRuntime(request.getHookTypeName(), request.getRequestData().getProviderCredentials(),
234245
request.getRequestData().getProviderLogGroupName(), request.getAwsAccountId(),
235246
request.getRequestData().getHookEncryptionKeyArn(), request.getRequestData().getHookEncryptionKeyRole());
236247

248+
if (isPayloadRemote) {
249+
Map<String, Object> targetModelData = retrieveHookInvocationPayloadFromS3(request.getRequestData().getPayload());
250+
251+
request.getRequestData().setTargetModel(targetModelData);
252+
}
253+
237254
// transform the request object to pass to caller
238255
HookHandlerRequest hookHandlerRequest = transform(request);
239256
ConfigurationT typeConfiguration = request.getHookModel();
@@ -366,6 +383,50 @@ private void writeResponse(final OutputStream outputStream, final HookProgressEv
366383
outputStream.flush();
367384
}
368385

386+
public Map<String, Object> retrieveHookInvocationPayloadFromS3(final String s3PresignedUrl) {
387+
if (s3PresignedUrl != null) {
388+
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
389+
390+
try {
391+
URL presignedUrl = new URL(s3PresignedUrl);
392+
SdkHttpRequest httpRequest = SdkHttpRequest.builder().method(SdkHttpMethod.GET).uri(presignedUrl.toURI()).build();
393+
394+
HttpExecuteRequest executeRequest = HttpExecuteRequest.builder().request(httpRequest).build();
395+
396+
HttpExecuteResponse response = HTTP_CLIENT.prepareRequest(executeRequest).call();
397+
398+
response.responseBody().ifPresentOrElse(abortableInputStream -> {
399+
try {
400+
IoUtils.copy(abortableInputStream, byteArrayOutputStream);
401+
} catch (IOException e) {
402+
throw new RuntimeException(e);
403+
}
404+
}, () -> loggerProxy.log("Hook invocation payload is empty."));
405+
406+
String str = byteArrayOutputStream.toString(StandardCharsets.UTF_8);
407+
408+
return this.serializer.deserialize(str, hookStackPayloadS3TypeReference);
409+
} catch (RuntimeException | IOException | URISyntaxException exp) {
410+
loggerProxy.log("Failed to retrieve hook invocation payload" + exp.toString());
411+
}
412+
}
413+
return Collections.emptyMap();
414+
}
415+
416+
@VisibleForTesting
417+
protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) {
418+
if (hookRequestData == null) {
419+
throw new TerminalException("Invalid request object received. Target Model can not be null.");
420+
}
421+
422+
if ((hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty())
423+
&& hookRequestData.getPayload() == null) {
424+
throw new TerminalException("No payload data set.");
425+
}
426+
427+
return (hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty());
428+
}
429+
369430
/**
370431
* Transforms the incoming request to the subset of typed models which the
371432
* handler implementor needs

‎src/main/java/software/amazon/cloudformation/proxy/OperationStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ public enum OperationStatus {
1818
PENDING,
1919
IN_PROGRESS,
2020
SUCCESS,
21+
CHANGE_SET_SUCCESS_SKIP_STACK_HOOK,
2122
FAILED
2223
}

‎src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public class HookRequestData {
2929
private String targetType;
3030
private String targetLogicalId;
3131
private Map<String, Object> targetModel;
32+
private String payload;
3233
private String callerCredentials;
3334
private String providerCredentials;
3435
private String providerLogGroupName;

‎src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,6 @@ public enum HookStatus {
1818
PENDING,
1919
IN_PROGRESS,
2020
SUCCESS,
21+
CHANGE_SET_SUCCESS_SKIP_STACK_HOOK,
2122
FAILED
2223
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package software.amazon.cloudformation.proxy.hook.targetmodel;
16+
17+
import com.fasterxml.jackson.annotation.JsonProperty;
18+
import lombok.AllArgsConstructor;
19+
import lombok.Builder;
20+
import lombok.Data;
21+
import lombok.NoArgsConstructor;
22+
23+
@Data
24+
@Builder
25+
@AllArgsConstructor
26+
@NoArgsConstructor
27+
public class ChangedResource {
28+
@JsonProperty("LogicalResourceId")
29+
private String logicalResourceId;
30+
31+
@JsonProperty("ResourceType")
32+
private String resourceType;
33+
34+
@JsonProperty("LineNumber")
35+
private Integer lineNumber;
36+
37+
@JsonProperty("Action")
38+
private String action;
39+
40+
@JsonProperty("ResourceProperties")
41+
private String resourceProperties;
42+
43+
@JsonProperty("PreviousResourceProperties")
44+
private String previousResourceProperties;
45+
}

‎src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetType.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,17 @@ public enum HookTargetType {
2626
* A target model meant to represent a target for a Resource Hook. This model
2727
* type will have properties specific to the resource type.
2828
*/
29-
RESOURCE;
29+
RESOURCE,
30+
31+
/**
32+
* A target model meant to represent a target for a Stack Hook. This model type
33+
* will have properties specific to the stack type.
34+
*/
35+
STACK,
36+
37+
/**
38+
* A target model meant to represent a target for a stack Change Set Hook. This
39+
* model type will have properties specific to the change set type.
40+
*/
41+
CHANGE_SET;
3042
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License").
5+
* You may not use this file except in compliance with the License.
6+
* A copy of the License is located at
7+
*
8+
* http://aws.amazon.com/apache2.0
9+
*
10+
* or in the "license" file accompanying this file. This file is distributed
11+
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12+
* express or implied. See the License for the specific language governing
13+
* permissions and limitations under the License.
14+
*/
15+
package software.amazon.cloudformation.proxy.hook.targetmodel;
16+
17+
import com.fasterxml.jackson.annotation.JsonAutoDetect;
18+
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
19+
import com.fasterxml.jackson.annotation.JsonProperty;
20+
import com.fasterxml.jackson.core.type.TypeReference;
21+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
22+
import java.util.List;
23+
import lombok.EqualsAndHashCode;
24+
import lombok.Getter;
25+
import lombok.NoArgsConstructor;
26+
import lombok.ToString;
27+
28+
@EqualsAndHashCode(callSuper = false)
29+
@Getter
30+
@NoArgsConstructor
31+
@ToString
32+
@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE)
33+
@JsonDeserialize(as = StackHookTargetModel.class)
34+
public class StackHookTargetModel extends HookTargetModel {
35+
private static final TypeReference<StackHookTargetModel> MODEL_REFERENCE = new TypeReference<StackHookTargetModel>() {
36+
};
37+
38+
@JsonProperty("Template")
39+
private Object template;
40+
41+
@JsonProperty("PreviousTemplate")
42+
private Object previousTemplate;
43+
44+
@JsonProperty("ResolvedTemplate")
45+
private Object resolvedTemplate;
46+
47+
@JsonProperty("ChangedResources")
48+
private List<ChangedResource> changedResources;
49+
50+
@Override
51+
public TypeReference<? extends HookTarget> getHookTargetTypeReference() {
52+
return null;
53+
}
54+
55+
@Override
56+
public TypeReference<? extends HookTargetModel> getTargetModelTypeReference() {
57+
return MODEL_REFERENCE;
58+
}
59+
60+
@Override
61+
public final HookTargetType getHookTargetType() {
62+
return HookTargetType.STACK;
63+
}
64+
}

‎src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.nio.charset.StandardCharsets;
2020
import java.util.LinkedList;
2121
import java.util.List;
22+
import java.util.Map;
2223
import java.util.Queue;
2324
import lombok.Data;
2425
import lombok.EqualsAndHashCode;
@@ -32,8 +33,10 @@
3233
import software.amazon.cloudformation.metrics.MetricsPublisher;
3334
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
3435
import software.amazon.cloudformation.proxy.ProgressEvent;
36+
import software.amazon.cloudformation.proxy.hook.HookContext;
3537
import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
3638
import software.amazon.cloudformation.proxy.hook.HookInvocationRequest;
39+
import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel;
3740
import software.amazon.cloudformation.resource.SchemaValidator;
3841
import software.amazon.cloudformation.resource.Serializer;
3942

@@ -44,6 +47,8 @@
4447
@EqualsAndHashCode(callSuper = true)
4548
public class HookLambdaWrapperOverride extends HookLambdaWrapper<TestModel, TestContext, TestConfigurationModel> {
4649

50+
private Map<String, Object> hookInvocationPayloadFromS3;
51+
4752
/**
4853
* This .ctor provided for testing
4954
*/
@@ -112,11 +117,29 @@ public void enqueueResponses(final List<ProgressEvent<TestModel, TestContext>> r
112117

113118
@Override
114119
protected HookHandlerRequest transform(final HookInvocationRequest<TestConfigurationModel, TestContext> request) {
115-
return transformResponse;
120+
this.request = HookHandlerRequest.builder().clientRequestToken(request.getClientRequestToken())
121+
.hookContext(HookContext.builder().awsAccountId(request.getAwsAccountId()).stackId(request.getStackId())
122+
.changeSetId(request.getChangeSetId()).hookTypeName(request.getHookTypeName())
123+
.hookTypeVersion(request.getHookTypeVersion()).invocationPoint(request.getActionInvocationPoint())
124+
.targetName(request.getRequestData().getTargetName()).targetType(request.getRequestData().getTargetType())
125+
.targetLogicalId(request.getRequestData().getTargetLogicalId())
126+
.targetModel(HookTargetModel.of(request.getRequestData().getTargetModel())).build())
127+
.build();
128+
129+
return this.request;
116130
}
117131

118132
public HookHandlerRequest transformResponse;
119133

134+
@Override
135+
public Map<String, Object> retrieveHookInvocationPayloadFromS3(final String s3PresignedUrl) {
136+
return hookInvocationPayloadFromS3;
137+
}
138+
139+
public void setHookInvocationPayloadFromS3(Map<String, Object> input) {
140+
hookInvocationPayloadFromS3 = input;
141+
}
142+
120143
@Override
121144
protected TypeReference<HookInvocationRequest<TestConfigurationModel, TestContext>> getTypeReference() {
122145
return new TypeReference<HookInvocationRequest<TestConfigurationModel, TestContext>>() {

‎src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java

Lines changed: 108 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,29 @@
2323
import com.amazonaws.services.lambda.runtime.Context;
2424
import com.amazonaws.services.lambda.runtime.LambdaLogger;
2525
import com.fasterxml.jackson.core.type.TypeReference;
26+
import com.google.common.collect.ImmutableList;
27+
import com.google.common.collect.ImmutableMap;
2628
import java.io.ByteArrayOutputStream;
2729
import java.io.File;
2830
import java.io.FileInputStream;
2931
import java.io.FileNotFoundException;
3032
import java.io.IOException;
3133
import java.io.InputStream;
3234
import java.io.OutputStream;
35+
import java.util.Collections;
36+
import java.util.List;
37+
import java.util.Map;
38+
import org.junit.jupiter.api.Assertions;
3339
import org.junit.jupiter.api.BeforeEach;
40+
import org.junit.jupiter.api.Test;
3441
import org.junit.jupiter.api.extension.ExtendWith;
3542
import org.junit.jupiter.params.ParameterizedTest;
3643
import org.junit.jupiter.params.provider.CsvSource;
3744
import org.mockito.Mock;
3845
import org.mockito.junit.jupiter.MockitoExtension;
3946
import software.amazon.awssdk.http.SdkHttpClient;
4047
import software.amazon.cloudformation.encryption.KMSCipher;
48+
import software.amazon.cloudformation.exceptions.TerminalException;
4149
import software.amazon.cloudformation.injection.CredentialsProvider;
4250
import software.amazon.cloudformation.loggers.CloudWatchLogPublisher;
4351
import software.amazon.cloudformation.loggers.LogPublisher;
@@ -48,7 +56,10 @@
4856
import software.amazon.cloudformation.proxy.ProgressEvent;
4957
import software.amazon.cloudformation.proxy.hook.HookHandlerRequest;
5058
import software.amazon.cloudformation.proxy.hook.HookProgressEvent;
59+
import software.amazon.cloudformation.proxy.hook.HookRequestData;
5160
import software.amazon.cloudformation.proxy.hook.HookStatus;
61+
import software.amazon.cloudformation.proxy.hook.targetmodel.ChangedResource;
62+
import software.amazon.cloudformation.proxy.hook.targetmodel.StackHookTargetModel;
5263
import software.amazon.cloudformation.resource.SchemaValidator;
5364
import software.amazon.cloudformation.resource.Serializer;
5465

@@ -168,7 +179,6 @@ public void invokeHandler_CompleteSynchronously_returnsSuccess(final String requ
168179

169180
// assert handler receives correct injections
170181
assertThat(wrapper.awsClientProxy).isNotNull();
171-
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
172182
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
173183
assertThat(wrapper.callbackContext).isNull();
174184
}
@@ -205,7 +215,6 @@ public void invokeHandler_WithResourceProperties_returnsSuccess(final String req
205215

206216
// assert handler receives correct injections
207217
assertThat(wrapper.awsClientProxy).isNotNull();
208-
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
209218
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
210219
assertThat(wrapper.callbackContext).isNull();
211220
}
@@ -242,7 +251,6 @@ public void invokeHandler_WithResourcePropertiesAndExtraneousFields_returnsSucce
242251

243252
// assert handler receives correct injections
244253
assertThat(wrapper.awsClientProxy).isNotNull();
245-
assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest);
246254
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
247255
assertThat(wrapper.callbackContext).isNull();
248256
}
@@ -279,7 +287,6 @@ public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSucce
279287

280288
// assert handler receives correct injections
281289
assertThat(wrapperStrictDeserialize.awsClientProxy).isNotNull();
282-
assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(hookHandlerRequest);
283290
assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(invocationPoint);
284291
assertThat(wrapperStrictDeserialize.callbackContext).isNull();
285292
}
@@ -323,6 +330,103 @@ public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSucce
323330
}
324331
}
325332

333+
@ParameterizedTest
334+
@CsvSource({ "preCreate.request.with-stack-level-hook.json,CREATE_PRE_PROVISION" })
335+
public void invokeHandler_WithStackLevelHook_returnsSuccess(final String requestDataPath, final String invocationPointString)
336+
throws IOException {
337+
final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString);
338+
339+
final ProgressEvent<TestModel,
340+
TestContext> pe = ProgressEvent.<TestModel, TestContext>builder().status(OperationStatus.SUCCESS).build();
341+
wrapper.setInvokeHandlerResponse(pe);
342+
343+
lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));
344+
345+
wrapper.setHookInvocationPayloadFromS3(Map.of(
346+
"Template", "template string here",
347+
"PreviousTemplate", "previous template string here",
348+
"ResolvedTemplate", "resolved template string here",
349+
"ChangedResources", List.of(
350+
Map.of(
351+
"LogicalResourceId", "SomeLogicalResourceId",
352+
"ResourceType", "AWS::S3::Bucket",
353+
"Action", "CREATE",
354+
"LineNumber", 3,
355+
"ResourceProperties", "<Resource Properties as json string>",
356+
"PreviousResourceProperties", "<Resource Properties as json string>"
357+
)
358+
)
359+
));
360+
361+
try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
362+
final Context context = getLambdaContext();
363+
364+
wrapper.handleRequest(in, out, context);
365+
366+
// verify initialiseRuntime was called and initialised dependencies
367+
verifyInitialiseRuntime();
368+
369+
// verify output response
370+
verifyHandlerResponse(out,
371+
HookProgressEvent.<TestContext>builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build());
372+
373+
// assert handler receives correct injections
374+
assertThat(wrapper.awsClientProxy).isNotNull();
375+
assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint);
376+
assertThat(wrapper.callbackContext).isNull();
377+
378+
assertThat(wrapper.getRequest().getHookContext().getTargetType()).isEqualTo("STACK");
379+
assertThat(wrapper.getRequest().getHookContext().getTargetName()).isEqualTo("STACK");
380+
assertThat(wrapper.getRequest().getHookContext().getTargetLogicalId()).isEqualTo("myStack");
381+
382+
StackHookTargetModel stackHookTargetModel = wrapper.getRequest().getHookContext()
383+
.getTargetModel(StackHookTargetModel.class);
384+
assertThat(stackHookTargetModel.getTemplate()).isEqualTo("template string here");
385+
assertThat(stackHookTargetModel.getPreviousTemplate()).isEqualTo("previous template string here");
386+
assertThat(stackHookTargetModel.getResolvedTemplate()).isEqualTo("resolved template string here");
387+
assertThat(stackHookTargetModel.getChangedResources().size()).isEqualTo(1);
388+
389+
ChangedResource expectedChangedResource = ChangedResource.builder().logicalResourceId("SomeLogicalResourceId")
390+
.resourceType("AWS::S3::Bucket").lineNumber(3).action("CREATE")
391+
.resourceProperties("<Resource Properties as json string>")
392+
.previousResourceProperties("<Resource Properties as json string>").build();
393+
assertThat(stackHookTargetModel.getChangedResources().get(0)).isEqualTo(expectedChangedResource);
394+
}
395+
}
396+
397+
@Test
398+
public void testIsHookInvocationPayloadRemote() {
399+
List<HookRequestData> invalidHookRequestDataObjects = ImmutableList.of(
400+
HookRequestData.builder().targetModel(null).build(),
401+
HookRequestData.builder().targetModel(null).payload(null).build(),
402+
HookRequestData.builder().targetModel(Collections.emptyMap()).payload(null).build()
403+
);
404+
405+
invalidHookRequestDataObjects.forEach(requestData -> {
406+
Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(requestData));
407+
});
408+
409+
Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(null));
410+
411+
HookRequestData bothFieldsPopulated = HookRequestData.builder()
412+
.targetModel(ImmutableMap.of("foo", "bar"))
413+
.payload("http://s3PresignedUrl")
414+
.build();
415+
HookRequestData onlyTargetModelPopulated = HookRequestData.builder()
416+
.targetModel(ImmutableMap.of("foo", "bar"))
417+
.payload(null).build();
418+
HookRequestData onlyPayloadPopulated = HookRequestData.builder()
419+
.targetModel(Collections.emptyMap())
420+
.payload("http://s3PresignedUrl").build();
421+
HookRequestData onlyPayloadPopulatedWithNullTargetModel = HookRequestData.builder().targetModel(null)
422+
.payload("http://s3PresignedUrl").build();
423+
424+
Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated));
425+
Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated));
426+
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated));
427+
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulatedWithNullTargetModel));
428+
}
429+
326430
private final String expectedStringWhenStrictDeserializingWithExtraneousFields = "Unrecognized field \"targetName\" (class software.amazon.cloudformation.proxy.hook.HookInvocationRequest), not marked as ignorable (10 known properties: \"requestContext\", \"stackId\", \"clientRequestToken\", \"hookModel\", \"hookTypeName\", \"requestData\", \"actionInvocationPoint\", \"awsAccountId\", \"changeSetId\", \"hookTypeVersion\"])\n"
327431
+ " at [Source: (String)\"{\n" + " \"clientRequestToken\": \"123456\",\n" + " \"awsAccountId\": \"123456789012\",\n"
328432
+ " \"stackId\": \"arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968\",\n"
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"clientRequestToken": "123456",
3+
"awsAccountId": "123456789012",
4+
"stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968",
5+
"changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000",
6+
"hookTypeName": "AWS::Test::TestModel",
7+
"hookTypeVersion": "1.0",
8+
"hookModel": {
9+
"property1": "abc",
10+
"property2": 123
11+
},
12+
"actionInvocationPoint": "CREATE_PRE_PROVISION",
13+
"requestData": {
14+
"targetName": "STACK",
15+
"targetType": "STACK",
16+
"targetLogicalId": "myStack",
17+
"targetModel": {},
18+
"payload": "http://someS3PresignedUrl",
19+
"callerCredentials": "callerCredentials",
20+
"providerCredentials": "providerCredentials",
21+
"providerLogGroupName": "providerLoggingGroupName",
22+
"hookEncryptionKeyArn": "hookEncryptionKeyArn",
23+
"hookEncryptionKeyRole": "hookEncryptionKeyRole"
24+
},
25+
"requestContext": {}
26+
}

‎src/test/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModelTest.java

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@
1616

1717
import com.fasterxml.jackson.core.type.TypeReference;
1818
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import com.google.common.collect.ImmutableList;
1920
import com.google.common.collect.ImmutableMap;
2021
import java.io.File;
2122
import java.io.FileInputStream;
2223
import java.io.IOException;
2324
import java.nio.charset.StandardCharsets;
2425
import java.util.Collections;
2526
import java.util.HashMap;
27+
import java.util.List;
2628
import java.util.Map;
2729
import java.util.Objects;
2830
import java.util.stream.Stream;
@@ -199,6 +201,93 @@ public void testHookTargetModelWithAdditionalProperties() throws Exception {
199201
OBJECT_MAPPER.writeValueAsString(resourceProperties));
200202
}
201203

204+
@Test
205+
public void testStackHookTargetModel() throws Exception {
206+
final String template = "{\"key1\":\"value1\"}";
207+
final String previousTemplate = "{\"previousKey1\":\"previousValue1\"}";
208+
final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}";
209+
final List<ChangedResource> changedResources = ImmutableList
210+
.of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket")
211+
.action("CREATE").lineNumber(11).previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"")
212+
.resourceProperties("{\"BucketName\": \"some-bucket-name\"").build());
213+
214+
final Map<String, Object> targetModelMap = ImmutableMap.of("Template", template, "PreviousTemplate", previousTemplate,
215+
"ResolvedTemplate", resolvedTemplate, "ChangedResources", changedResources);
216+
217+
final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class);
218+
219+
Assertions.assertEquals(template, targetModel.getTemplate());
220+
Assertions.assertEquals(previousTemplate, targetModel.getPreviousTemplate());
221+
Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate());
222+
Assertions.assertEquals(changedResources, targetModel.getChangedResources());
223+
Assertions.assertNull(targetModel.getHookTargetTypeReference());
224+
Assertions.assertEquals(
225+
"{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":\"{\\\"previousKey1\\\":\\\""
226+
+ "previousValue1\\\"}\",\"ResolvedTemplate\":\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\",\"ChangedResources\""
227+
+ ":[{\"LogicalResourceId\":\"SomeLogicalResourceId\",\"ResourceType\":\"AWS::S3::Bucket\",\"LineNumber\":"
228+
+ "11,\"Action\":\"CREATE\",\"ResourceProperties\":\"{\\\"BucketName\\\": \\\"some-bucket-name\\\"\","
229+
+ "\"PreviousResourceProperties\":\"{\\\"BucketName\\\": \\\"some-prev-bucket-name\\\"\"}]}",
230+
OBJECT_MAPPER.writeValueAsString(targetModel));
231+
}
232+
233+
@Test
234+
public void testStackHookTargetModelWithAdditionalPropertiesInInput() throws Exception {
235+
final String template = "{\"key1\":\"value1\"}";
236+
final String previousTemplate = "{\"previousKey1\":\"previousValue1\"}";
237+
final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}";
238+
final String extraneousProperty = "{\"extraKey\":\"extraValue\"}";
239+
final List<ChangedResource> changedResources = ImmutableList
240+
.of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket")
241+
.action("CREATE").lineNumber(11).previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"")
242+
.resourceProperties("{\"BucketName\": \"some-bucket-name\"").build());
243+
244+
final Map<String, Object> targetModelMap = ImmutableMap.of("Template", template, "PreviousTemplate", previousTemplate,
245+
"ResolvedTemplate", resolvedTemplate, "ChangedResources", changedResources, "ExtraProperty", extraneousProperty);
246+
247+
final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class);
248+
249+
Assertions.assertEquals(template, targetModel.getTemplate());
250+
Assertions.assertEquals(previousTemplate, targetModel.getPreviousTemplate());
251+
Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate());
252+
Assertions.assertEquals(changedResources, targetModel.getChangedResources());
253+
Assertions.assertNull(targetModel.getHookTargetTypeReference());
254+
255+
Assertions.assertEquals("{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":\"{\\\"previousKey1\\\":"
256+
+ "\\\"previousValue1\\\"}\",\"ResolvedTemplate\":\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\","
257+
+ "\"ChangedResources\":[{\"LogicalResourceId\":\"SomeLogicalResourceId\",\"ResourceType\":"
258+
+ "\"AWS::S3::Bucket\",\"LineNumber\":11,\"Action\":\"CREATE\",\"ResourceProperties\":\"{"
259+
+ "\\\"BucketName\\\": \\\"some-bucket-name\\\"\",\"PreviousResourceProperties\":\"{\\\"BucketName\\\":"
260+
+ " \\\"some-prev-bucket-name\\\"\"}]}", OBJECT_MAPPER.writeValueAsString(targetModel));
261+
}
262+
263+
@Test
264+
public void testStackHookTargetModelWithMissingPropertiesInInput() throws Exception {
265+
final String template = "{\"key1\":\"value1\"}";
266+
final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}";
267+
final List<ChangedResource> changedResources = ImmutableList
268+
.of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket")
269+
.action("CREATE").previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"")
270+
.resourceProperties("{\"BucketName\": \"some-bucket-name\"").build());
271+
272+
final Map<String, Object> targetModelMap = ImmutableMap.of("Template", template, "ResolvedTemplate", resolvedTemplate,
273+
"ChangedResources", changedResources);
274+
275+
final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class);
276+
277+
Assertions.assertEquals(template, targetModel.getTemplate());
278+
Assertions.assertNull(targetModel.getPreviousTemplate());
279+
Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate());
280+
Assertions.assertEquals(changedResources, targetModel.getChangedResources());
281+
Assertions.assertNull(targetModel.getHookTargetTypeReference());
282+
Assertions.assertEquals(
283+
"{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":null,\"ResolvedTemplate\":"
284+
+ "\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\",\"ChangedResources\":[{\"LogicalResourceId\":"
285+
+ "\"SomeLogicalResourceId\",\"ResourceType\":\"AWS::S3::Bucket\",\"LineNumber\":null,\"Action\":"
286+
+ "\"CREATE\",\"ResourceProperties\":\"{\\\"BucketName\\\": \\\"some-bucket-name\\\"\","
287+
+ "\"PreviousResourceProperties\":\"{\\\"BucketName\\\": \\\"some-prev-bucket-name\\\"\"}]}",
288+
OBJECT_MAPPER.writeValueAsString(targetModel));
289+
}
290+
202291
@Test
203292
public void testHookTargetTypeWithNullValue() {
204293
final HookTargetModel targetModel = HookTargetModel.of(null);

0 commit comments

Comments
 (0)
Please sign in to comment.