Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ repos:
hooks:
- id: bandit
files: "^python/"
additional_dependencies: [pbr]
- repo: local
hooks:
- id: pylint-local
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>software.amazon.cloudformation</groupId>
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
<version>2.2.3</version>
<version>2.2.4</version>
<name>AWS CloudFormation RPDK Java Plugin</name>
<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.
</description>
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def find_version(*file_paths):
# package_data -> use MANIFEST.in instead
include_package_data=True,
zip_safe=True,
install_requires=["cloudformation-cli>=0.2.23"],
install_requires=["cloudformation-cli>=0.2.23", "setuptools"],
python_requires=">=3.8",
entry_points={"rpdk.v1.languages": ["java = rpdk.java.codegen:JavaLanguagePlugin"]},
license="Apache License 2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ protected void writeResponse(final OutputStream outputStream, final ProgressEven
response.setResult(null);
}

if (response.getAnnotations() != null) {
// Same as above: remove any non-resource specific fields
// from response - in this case, expunge Hook Annotations.
response.setAnnotations(null);
}

String output = this.serializer.serialize(response);
outputStream.write(output.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ private HookProgressEvent<CallbackT> createProgressResponse(final ProgressEvent<
if (request != null) {
response.setClientRequestToken(request.getClientRequestToken());
}
response.setAnnotations(progressEvent.getAnnotations());

return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import software.amazon.cloudformation.proxy.hook.HookAnnotation;

@Data
@AllArgsConstructor
Expand Down Expand Up @@ -81,6 +82,15 @@ public class ProgressEvent<ResourceT, CallbackT> {
*/
private String nextToken;

/**
* The optional list of HookAnnotation objects that, if used by a CloudFormation
* Hook, contain additional, user-defined metadata and information on the
* results of a hook's evaluation.
*
* Note: this field is ignored for resource handlers.
*/
private List<HookAnnotation> annotations;

/**
* Convenience method for constructing a FAILED response
*
Expand Down Expand Up @@ -158,7 +168,8 @@ public static <ResourceT, CallbackT> ProgressEvent<ResourceT, CallbackT> success

public static <ResourceT,
CallbackT> ProgressEvent<ResourceT, CallbackT> success(ResourceT model, CallbackT cxt, String message) {
return success(model, cxt, message, null);
return ProgressEvent.<ResourceT, CallbackT>builder().resourceModel(model).callbackContext(cxt).message(message)
.status(OperationStatus.SUCCESS).build();
}

public static <ResourceT,
Expand All @@ -167,6 +178,20 @@ CallbackT> ProgressEvent<ResourceT, CallbackT> success(ResourceT model, Callback
.result(result).status(OperationStatus.SUCCESS).build();
}

public static <ResourceT, CallbackT>
ProgressEvent<ResourceT, CallbackT>
success(ResourceT model, CallbackT cxt, String message, List<HookAnnotation> annotations) {
return ProgressEvent.<ResourceT, CallbackT>builder().resourceModel(model).callbackContext(cxt).message(message)
.annotations(annotations).status(OperationStatus.SUCCESS).build();
}

public static <ResourceT, CallbackT>
ProgressEvent<ResourceT, CallbackT>
success(ResourceT model, CallbackT cxt, String message, String result, List<HookAnnotation> annotations) {
return ProgressEvent.<ResourceT, CallbackT>builder().resourceModel(model).callbackContext(cxt).message(message)
.result(result).annotations(annotations).status(OperationStatus.SUCCESS).build();
}

public ProgressEvent<ResourceT, CallbackT>
onSuccess(Function<ProgressEvent<ResourceT, CallbackT>, ProgressEvent<ResourceT, CallbackT>> func) {
return (status != null && status == OperationStatus.SUCCESS) ? func.apply(this) : this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2010-2019 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 software.amazon.cloudformation.proxy.hook;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class HookAnnotation {
/**
* The name of the annotation; this is mandatory.
*/
private String annotationName;

/**
* The status for the hook annotation: this is mandatory.
*/
private HookAnnotationStatus status;

/**
* The optional status message for the annotation.
*/
private String statusMessage;

/**
* The optional remediation message for the annotation.
*/
private String remediationMessage;

/**
* The optional remediation link for the annotation.
*/
private String remediationLink;

/**
* The optional severity level for the annotation.
*/
private HookAnnotationSeverityLevel severityLevel;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2010-2019 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 software.amazon.cloudformation.proxy.hook;

public enum HookAnnotationSeverityLevel {
INFORMATIONAL,
LOW,
MEDIUM,
HIGH,
CRITICAL,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2010-2019 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 software.amazon.cloudformation.proxy.hook;

public enum HookAnnotationStatus {
PASSED,
FAILED,
SKIPPED,
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package software.amazon.cloudformation.proxy.hook;

import com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.List;
import java.util.function.Function;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand Down Expand Up @@ -71,6 +72,13 @@ public class HookProgressEvent<CallbackT> {
*/
private String result;

/**
* The optional list of HookAnnotation objects that, if used by a CloudFormation
* Hook, contain additional, user-defined metadata and information on the
* results of a hook's evaluation.
*/
private List<HookAnnotation> annotations;

/**
* Convenience method for constructing a FAILED response
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ private void verifyHandlerResponse(final OutputStream out, final HookProgressEve
assertThat(handlerResponse.getResult()).isEqualTo(expected.getResult());
assertThat(handlerResponse.getCallbackContext()).isEqualTo(expected.getCallbackContext());
assertThat(handlerResponse.getCallbackDelaySeconds()).isEqualTo(expected.getCallbackDelaySeconds());
assertThat(handlerResponse.getAnnotations()).isEqualTo(expected.getAnnotations());
}

@ParameterizedTest
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ private void verifyHandlerResponse(final OutputStream out, final HookProgressEve
assertThat(handlerResponse.getResult()).isEqualTo(expected.getResult());
assertThat(handlerResponse.getCallbackContext()).isEqualTo(expected.getCallbackContext());
assertThat(handlerResponse.getCallbackDelaySeconds()).isEqualTo(expected.getCallbackDelaySeconds());
assertThat(handlerResponse.getAnnotations()).isEqualTo(expected.getAnnotations());
}

@ParameterizedTest
Expand Down Expand Up @@ -342,21 +343,11 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request

lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123"));

wrapper.setHookInvocationPayloadFromS3(Map.of(
"Template", "template string here",
"PreviousTemplate", "previous template string here",
"ResolvedTemplate", "resolved template string here",
"ChangedResources", List.of(
Map.of(
"LogicalResourceId", "SomeLogicalResourceId",
"ResourceType", "AWS::S3::Bucket",
"Action", "CREATE",
"LineNumber", 3,
"ResourceProperties", "<Resource Properties as json string>",
"PreviousResourceProperties", "<Resource Properties as json string>"
)
)
));
wrapper.setHookInvocationPayloadFromS3(Map.of("Template", "template string here", "PreviousTemplate",
"previous template string here", "ResolvedTemplate", "resolved template string here", "ChangedResources",
List.of(Map.of("LogicalResourceId", "SomeLogicalResourceId", "ResourceType", "AWS::S3::Bucket", "Action", "CREATE",
"LineNumber", 3, "ResourceProperties", "<Resource Properties as json string>", "PreviousResourceProperties",
"<Resource Properties as json string>"))));

try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) {
final Context context = getLambdaContext();
Expand Down Expand Up @@ -394,38 +385,33 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request
}
}

@Test
public void testIsHookInvocationPayloadRemote() {
List<HookRequestData> invalidHookRequestDataObjects = ImmutableList.of(
HookRequestData.builder().targetModel(null).build(),
HookRequestData.builder().targetModel(null).payload(null).build(),
HookRequestData.builder().targetModel(Collections.emptyMap()).payload(null).build()
);
@Test
public void testIsHookInvocationPayloadRemote() {
List<
HookRequestData> invalidHookRequestDataObjects = ImmutableList.of(HookRequestData.builder().targetModel(null).build(),
HookRequestData.builder().targetModel(null).payload(null).build(),
HookRequestData.builder().targetModel(Collections.emptyMap()).payload(null).build());

invalidHookRequestDataObjects.forEach(requestData -> {
invalidHookRequestDataObjects.forEach(requestData -> {
Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(requestData));
});

Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(null));

HookRequestData bothFieldsPopulated = HookRequestData.builder()
.targetModel(ImmutableMap.of("foo", "bar"))
.payload("http://s3PresignedUrl")
.build();
HookRequestData onlyTargetModelPopulated = HookRequestData.builder()
.targetModel(ImmutableMap.of("foo", "bar"))
.payload(null).build();
HookRequestData onlyPayloadPopulated = HookRequestData.builder()
.targetModel(Collections.emptyMap())
.payload("http://s3PresignedUrl").build();
HookRequestData onlyPayloadPopulatedWithNullTargetModel = HookRequestData.builder().targetModel(null)
.payload("http://s3PresignedUrl").build();

Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated));
Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated));
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated));
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulatedWithNullTargetModel));
}
});

Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(null));

HookRequestData bothFieldsPopulated = HookRequestData.builder().targetModel(ImmutableMap.of("foo", "bar"))
.payload("http://s3PresignedUrl").build();
HookRequestData onlyTargetModelPopulated = HookRequestData.builder().targetModel(ImmutableMap.of("foo", "bar"))
.payload(null).build();
HookRequestData onlyPayloadPopulated = HookRequestData.builder().targetModel(Collections.emptyMap())
.payload("http://s3PresignedUrl").build();
HookRequestData onlyPayloadPopulatedWithNullTargetModel = HookRequestData.builder().targetModel(null)
.payload("http://s3PresignedUrl").build();

Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated));
Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated));
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated));
Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulatedWithNullTargetModel));
}

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"
+ " at [Source: (String)\"{\n" + " \"clientRequestToken\": \"123456\",\n" + " \"awsAccountId\": \"123456789012\",\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ private void verifyHandlerResponse(final OutputStream out, final HookProgressEve
assertThat(handlerResponse.getResult()).isEqualTo(expected.getResult());
assertThat(handlerResponse.getCallbackContext()).isEqualTo(expected.getCallbackContext());
assertThat(handlerResponse.getCallbackDelaySeconds()).isEqualTo(expected.getCallbackDelaySeconds());
assertThat(handlerResponse.getAnnotations()).isEqualTo(expected.getAnnotations());
}

@ParameterizedTest
Expand Down
Loading