Skip to content

Commit c6c8083

Browse files
author
diwakar
committed
- Adding Junit testing support for working with Named Profiles
- Added KMS Key support to easily whitelist services and work with KMS Adding proper KMS support with stabilization to handle KMS read inconsistencies - Added pre-commit hooks and other missing boostrap setup - Adding improved support to stubbing service clients - Added a CRUD testing framework that allows to test various runs for lifecycle management
1 parent 17926e0 commit c6c8083

File tree

11 files changed

+916
-139
lines changed

11 files changed

+916
-139
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
target/
2+
~*

README.md

Lines changed: 171 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,178 @@
1-
## My Project
1+
## AWS CloudFormation Java Plugin Test Framework
22

3-
TODO: Fill this README out!
3+
This provides an easier foundation for testing handlers for CRUD along with integated support for KMS. Developers
4+
can easily write sequence of CRUD lifecycle test with expectations and will be tested. There is also a mock based
5+
test based for local unit testing.
46

5-
Be sure to:
7+
The framework leverages support for [Named Profiles](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) that allows
8+
developers to test using roles and credentials, to test the exact way in which they expect to work inside CFN for their handlers. Here is the
9+
sample for now this can be used for injecting credentials using the role based profile specified.
10+
11+
**Sample AWS Named Profile Setup**
12+
13+
~/.aws/credentials
14+
...
15+
**cfn-assume-role**
16+
aws_access_key_id=[YOUR_ACCESS_KEY_ID]
17+
aws_secret_access_key=[YOUR_SECRET_ACCESS_KEY]
18+
...
19+
20+
~/.aws/config
21+
[profilei **cfn-integration**]
22+
role_arn = arn:aws:iam::<AWS_ACCOUNT_ID>:role/<ROLE_NAME>
23+
source_profile = *cfn-assume-role*
24+
25+
**Using the named profile for testing**
26+
27+
<b>How to setup IAM managed policies, user and role credentials for above setup</b>
28+
29+
<ol>
30+
<li><u>Create a Managed Policy for the user</u>
31+
Here the credentials section has an user credentials that is provided with only sts:assumeRole
32+
permission. Here is the policy that is associated with cfn-assume-role user in the account.
33+
34+
<pre>
35+
{
36+
"Version": "2012-10-17",
37+
"Statement": [
38+
{
39+
"Sid": "VisualEditor0",
40+
"Effect": "Allow",
41+
"Action": "sts:AssumeRole",
42+
"Resource": "*"
43+
}
44+
]
45+
}
46+
</pre>
47+
</li>
48+
<li><u>Create a Managed Policy for the services you are testing with</u>
49+
This is needed to test all integration for CRUD+L needed for logs. You can always narrow it down further.
50+
Recommended approach is to define the above policies as customer managed policies in IAM in the account and
51+
associate with the role and users as appropriate. This is an example policy to test CloudWatch LogGroup
52+
and KMS integration
53+
54+
<pre>
55+
{
56+
"Version": "2012-10-17",
57+
"Statement": [
58+
{
59+
"Sid": "VisualEditor0",
60+
"Effect": "Allow",
61+
"Action": [
62+
"kms:*",
63+
"[INSERT_YOUR_SERVICE]:*"
64+
],
65+
"Resource": "*"
66+
}
67+
]
68+
}
69+
</pre>
70+
</li>
71+
<li><u>Create a user cfn-assume-role with Managed Policy create in (1)</u>
72+
Download the access_key, secret_key for this user and add it to the credentials file under
73+
cfn-assume-role
74+
</li>
75+
<li><u>Create cfn-integration role with the con</u></li>
76+
<li><u>Update your poml.xml</u>
77+
Here is how to use this for unit testing. First add the dependency to you maven <u>pom.xml</u>
78+
79+
<pre>{@code
80+
<!-- for sts support to assume role setup above -->
81+
<dependency>
82+
<groupId>software.amazon.awssdk</groupId>
83+
<artifactId>sts</artifactId>
84+
<version>2.10.91</version>
85+
<scope>test</scope>
86+
</dependency>
87+
88+
<!-- for kms key handling support -->
89+
<dependency>
90+
<groupId>software.amazon.awssdk</groupId>
91+
<artifactId>kms</artifactId>
92+
<version>2.10.91</version>
93+
</dependency>
94+
95+
<dependency>
96+
<groupId>software.amazon.cloudformation.test</groupId>
97+
<artifactId>cloudformation-cli-java-plugin-testing-support</artifactId>
98+
<version>1.0-SNAPSHOT</version>
99+
<scope>test</scope>
100+
</dependency>
101+
}</pre>
102+
</li>
103+
</ol>
104+
105+
<b>How to use it?</b>
106+
<p>
107+
Sample code illustrating how to use this setup with KMS. To make scheduling the key for delete in case of abort to
108+
testing the key is aliased using the alias name [KEY_ALIAS](src/software/amazon/cloudformation/test/KMSKeyEnabledServiceIntegrationTestBase.java)
109+
The test when it runs to completion will automatically move the KMS key for delete. If test is rerun
110+
the KMS key will be made active again for the duration of he test run and disable and scheduled to be deleted.
111+
Regardless of how many times we run these tests there is only one key with the alias managed in the account.
112+
113+
To ensure that this test does not run for build environments like Travis etc. we enable is using system properties using
114+
{@link org.junit.jupiter.api.condition.EnabledIfSystemProperty}. To run the test with maven we would
115+
use
116+
117+
```
118+
mvn -Ddesktop=true test
119+
```
120+
121+
to run the test code shown below
122+
123+
```java
124+
125+
package software.amazon.logs.loggroup;
126+
127+
import org.junit.jupiter.api.*;
128+
import static org.assertj.core.api.Assertions.assertThat;
129+
130+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
131+
import software.amazon.cloudformation.proxy.*;
132+
import software.amazon.cloudformation.test.*;
133+
134+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) // we order the tests to follows C to U to D
135+
@ExtendWith(InjectProfileCredentials.class) // extend with profile based credentials
136+
@EnabledIfSystemProperty(named = "desktop", matches = "true")
137+
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // IMP PER_CLASS
138+
public class LifecycleTest extends KMSKeyEnabledServiceIntegrationTestBase {
139+
140+
//
141+
// At the annotation software.amazon.cloudformation.test.annotations.InjectSessionCredentials to the
142+
// constructor. This will inject the role's credentials
143+
//
144+
public LifecycleTest(@InjectSessionCredentials(profile = "cfn-integration") AwsSessionCredentials awsCredentials) {
145+
super(awsCredentials, ((apiCall, provided) -&gt; override));
146+
}
147+
...
148+
...
149+
@Order(300)
150+
@Test
151+
void addValidKMS() {
152+
final ResourceModel current = ResourceModel.builder().arn(model.getArn())
153+
.logGroupName(model.getLogGroupName()).retentionInDays(model.getRetentionInDays()).build();
154+
// Access a KMS key. The test ensures to only create one key and recycles despite any number of runs
155+
String kmsKeyId = getKmsKeyId();
156+
String kmsKeyArn = getKmsKeyArn();
157+
// Add your service to use KMS key
158+
addServiceAccess("logs", kmsKeyId);
159+
model.setKMSKey(kmsKeyArn);
160+
ProgressEvent&lt;ResourceModel, CallbackContext&gt; event = new UpdateHandler()
161+
.handleRequest(getProxy(), createRequest(model, current), null, getLoggerProxy());
162+
assertThat(event.isSuccess()).isTrue();
163+
model = event.getResourceModel();
164+
}
165+
...
166+
...
167+
}
168+
```
169+
170+
**See Also**
171+
172+
software.amazon.cloudformation.test.KMSKeyEnabledServiceIntegrationTestBase
173+
software.amazon.cloudformation.test.AbstractLifecycleTestBase
6174

7-
* Change the title in this README
8-
* Edit your repository description on GitHub
9175

10176
## License
11177

12178
This project is licensed under the Apache-2.0 License.
13-

pom.xml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@
5151
<groupId>org.assertj</groupId>
5252
<artifactId>assertj-core</artifactId>
5353
<version>3.12.2</version>
54-
<scope>test</scope>
5554
</dependency>
5655

5756
<!-- https://mvnrepository.com/artifact/org.slf4j/slf4j-api -->
@@ -82,7 +81,7 @@
8281
<dependency>
8382
<groupId>software.amazon.cloudformation</groupId>
8483
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
85-
<version>1.0.2</version>
84+
<version>1.0.3</version>
8685
</dependency>
8786

8887
<dependency>

src/main/java/software/amazon/cloudformation/test/AbstractLifecycleTestBase.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package software.amazon.cloudformation.test;
22

33
import org.junit.jupiter.api.TestInstance;
4-
import org.mockito.Mockito;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
56
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
7+
import software.amazon.cloudformation.loggers.LogPublisher;
68
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
79
import software.amazon.cloudformation.proxy.Credentials;
810
import software.amazon.cloudformation.proxy.DelayFactory;
@@ -19,7 +21,15 @@
1921
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
2022
public abstract class AbstractLifecycleTestBase {
2123

22-
private final LoggerProxy loggerProxy = Mockito.mock(LoggerProxy.class);
24+
private final LoggerProxy loggerProxy = new LoggerProxy() {{
25+
addLogPublisher(new LogPublisher(m -> m) {
26+
private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
27+
@Override
28+
protected void publishMessage(String message) {
29+
logger.info(message);
30+
}
31+
});
32+
}};
2333
private final AwsSessionCredentials sessionCredentials;
2434
private final AmazonWebServicesClientProxy proxy;
2535

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
package software.amazon.cloudformation.test;
2+
3+
import org.mockito.ArgumentMatcher;
4+
import org.mockito.Mockito;
5+
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
6+
import software.amazon.awssdk.auth.credentials.AwsSessionCredentials;
7+
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
8+
import software.amazon.awssdk.awscore.AwsRequest;
9+
import software.amazon.awssdk.awscore.AwsRequestOverrideConfiguration;
10+
import software.amazon.awssdk.awscore.AwsResponse;
11+
import software.amazon.awssdk.awscore.exception.AwsErrorDetails;
12+
import software.amazon.awssdk.awscore.exception.AwsServiceException;
13+
import software.amazon.awssdk.core.SdkClient;
14+
import software.amazon.awssdk.core.pagination.sync.SdkIterable;
15+
import software.amazon.awssdk.http.SdkHttpResponse;
16+
import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy;
17+
import software.amazon.cloudformation.proxy.Credentials;
18+
import software.amazon.cloudformation.proxy.LoggerProxy;
19+
import software.amazon.cloudformation.proxy.ProxyClient;
20+
21+
import javax.annotation.Nonnull;
22+
import java.time.Duration;
23+
import java.util.Collections;
24+
import java.util.List;
25+
import java.util.Map;
26+
import java.util.Optional;
27+
import java.util.concurrent.CompletableFuture;
28+
import java.util.function.Function;
29+
import java.util.function.Supplier;
30+
31+
@lombok.Getter
32+
public class AbstractMockTestBase<CLIENT extends SdkClient> {
33+
34+
protected final AwsSessionCredentials awsSessionCredential;
35+
protected final AwsCredentialsProvider v2CredentialsProvider;
36+
protected final AwsRequestOverrideConfiguration configuration;
37+
protected final LoggerProxy loggerProxy;
38+
protected final CLIENT serviceClient;
39+
protected final Supplier<Long> awsLambdaRuntime = () -> Duration.ofMinutes(15).toMillis();
40+
protected final AmazonWebServicesClientProxy proxy;
41+
protected final Credentials mockCredentials =
42+
new Credentials("mockAccessId", "mockSecretKey", "mockSessionToken");
43+
protected AbstractMockTestBase(Class<CLIENT> service) {
44+
serviceClient = Mockito.mock(service);
45+
loggerProxy = Mockito.mock(LoggerProxy.class);
46+
awsSessionCredential = AwsSessionCredentials.create(mockCredentials.getAccessKeyId(),
47+
mockCredentials.getSecretAccessKey(), mockCredentials.getSessionToken());
48+
v2CredentialsProvider = StaticCredentialsProvider.create(awsSessionCredential);
49+
configuration = AwsRequestOverrideConfiguration.builder()
50+
.credentialsProvider(v2CredentialsProvider)
51+
.build();
52+
proxy = new AmazonWebServicesClientProxy(
53+
loggerProxy,
54+
mockCredentials,
55+
awsLambdaRuntime
56+
) {
57+
@Override
58+
public <ClientT> ProxyClient<ClientT> newProxy(@Nonnull Supplier<ClientT> client) {
59+
return new ProxyClient<ClientT>() {
60+
@Override
61+
public <RequestT extends AwsRequest, ResponseT extends AwsResponse>
62+
ResponseT injectCredentialsAndInvokeV2(RequestT request,
63+
Function<RequestT, ResponseT> requestFunction) {
64+
return proxy.injectCredentialsAndInvokeV2(request, requestFunction);
65+
}
66+
67+
@Override
68+
public <RequestT extends AwsRequest, ResponseT extends AwsResponse> CompletableFuture<ResponseT>
69+
injectCredentialsAndInvokeV2Async(RequestT request, Function<RequestT, CompletableFuture<ResponseT>> requestFunction) {
70+
return proxy.injectCredentialsAndInvokeV2Async(request, requestFunction);
71+
}
72+
73+
@Override
74+
public <RequestT extends AwsRequest, ResponseT extends AwsResponse, IterableT extends SdkIterable<ResponseT>>
75+
IterableT
76+
injectCredentialsAndInvokeIterableV2(RequestT request, Function<RequestT, IterableT> requestFunction) {
77+
return proxy.injectCredentialsAndInvokeIterableV2(request, requestFunction);
78+
}
79+
80+
@SuppressWarnings("unchecked")
81+
@Override
82+
public ClientT client() {
83+
return (ClientT) serviceClient;
84+
}
85+
};
86+
}
87+
};
88+
}
89+
90+
protected static <T extends AwsServiceException, EB extends AwsServiceException.Builder>
91+
T
92+
make(final EB builder,
93+
final int status,
94+
final String message,
95+
final Class<T> type)
96+
{
97+
return type.cast(builder
98+
.awsErrorDetails(
99+
AwsErrorDetails.builder()
100+
.errorMessage(message)
101+
.errorCode(String.valueOf(status))
102+
.sdkHttpResponse(
103+
new SdkHttpResponse() {
104+
@Override
105+
public Optional<String> statusText() {
106+
return Optional.empty();
107+
}
108+
109+
@Override
110+
public int statusCode() {
111+
return status;
112+
}
113+
114+
@Override
115+
public Map<String, List<String>> headers() {
116+
return Collections.emptyMap();
117+
}
118+
119+
@Override
120+
public Builder toBuilder() {
121+
return null;
122+
}
123+
}
124+
)
125+
.build()
126+
)
127+
.build());
128+
}
129+
130+
protected static
131+
<T extends AwsRequest>
132+
ArgumentMatcher<T> argCmp(final T incoming) {
133+
return incoming::equalsBySdkFields;
134+
}
135+
}

0 commit comments

Comments
 (0)