diff --git a/.github/workflows/acceptance-test-run.yml b/.github/workflows/acceptance-test-run.yml
new file mode 100644
index 0000000..1a7ba69
--- /dev/null
+++ b/.github/workflows/acceptance-test-run.yml
@@ -0,0 +1,45 @@
+---
+name: Acceptenace Test Run
+on:
+ workflow_call:
+ inputs:
+ tests:
+ required: true
+ type: string
+ deletion-policy:
+ required: true
+ type: string # delete/retain
+permissions:
+ id-token: write
+ contents: read
+defaults:
+ run:
+ shell: bash
+jobs:
+ run:
+ runs-on: ['hyperenv', 'medium']
+ steps:
+ - uses: actions/checkout@v4
+ - uses: aws-actions/configure-aws-credentials@v4.0.1
+ with:
+ role-to-assume: arn:aws:iam::859548834666:role/github-openid-connect
+ role-session-name: github-actions-aws-in-action-code3
+ role-duration-seconds: 21600
+ aws-region: us-east-1
+ - uses: actions/setup-java@v3
+ with:
+ distribution: corretto
+ java-version: '8'
+ cache: maven
+ - working-directory: test
+ env:
+ DELETION_POLICY: ${{ inputs.deletion-policy }}
+ TEMPLATE_DIR: ../
+ run: mvn -B clean test site -Dtest=${{ inputs.tests }} -Dsurefire.reportNameSuffix=us-east-1
+ - uses: actions/upload-artifact@v3
+ if: failure()
+ with:
+ name: surefire-reports-us-east-1
+ path: |
+ test/target/surefire-reports/
+ test/target/site/
diff --git a/.github/workflows/acceptance-test.yml b/.github/workflows/acceptance-test.yml
new file mode 100644
index 0000000..d354afd
--- /dev/null
+++ b/.github/workflows/acceptance-test.yml
@@ -0,0 +1,39 @@
+---
+name: Acceptenace Test
+on:
+ workflow_dispatch:
+ inputs:
+ tests:
+ required: true
+ type: choice
+ default: '*'
+ options: # compile list with cd test/src && find . -name '*Test*' -execdir basename {} .java ';' | sort
+ - '*'
+ - Chapter02Test
+ - Chapter04Test
+ - Chapter05Test
+ - Chapter08Test
+ - Chapter09Test
+ - Chapter10Test
+ - Chapter11Test
+ - Chapter13Test
+ - Chapter14Test
+ - Chapter15Test
+ - Chapter17Test
+ - Chapter18Test
+ deletion-policy:
+ required: true
+ type: choice
+ default: delete
+ options:
+ - delete
+ - retain
+defaults:
+ run:
+ shell: bash
+jobs:
+ acceptance-test:
+ uses: ./.github/workflows/acceptance-test-run.yml
+ with:
+ tests: ${{ inputs.tests }}
+ deletion-policy: ${{ inputs.deletion-policy }}
diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml
new file mode 100644
index 0000000..c8ff584
--- /dev/null
+++ b/.github/workflows/acceptance-tests.yml
@@ -0,0 +1,21 @@
+---
+name: Acceptenace Tests
+on:
+ workflow_dispatch: {}
+ push:
+ branches:
+ - master
+ paths:
+ - '.github/workflows/acceptance-tests.yml'
+ - '*.yaml'
+ - 'test/**'
+concurrency: acceptenace-tests-${{ github.ref }}
+defaults:
+ run:
+ shell: bash
+jobs:
+ acceptance-test:
+ uses: ./.github/workflows/acceptance-test-run.yml
+ with:
+ tests: '*'
+ deletion-policy: delete
diff --git a/.github/workflows/latest.yml b/.github/workflows/latest.yml
index e15278a..1acc457 100644
--- a/.github/workflows/latest.yml
+++ b/.github/workflows/latest.yml
@@ -22,4 +22,4 @@ jobs:
aws-region: us-east-1
- name: latest
run: |
- aws s3 sync --delete --exact-timestamps --exclude ".git/*" --exclude ".github/*" ./ s3://awsinaction-code3/
+ aws s3 sync --delete --exact-timestamps --exclude ".git/*" --exclude ".github/*" --exclude "test/*" ./ s3://awsinaction-code3/
diff --git a/test/README.md b/test/README.md
new file mode 100644
index 0000000..4eab94f
--- /dev/null
+++ b/test/README.md
@@ -0,0 +1,63 @@
+# AWS in Action (3rd edition) Code tests
+
+Tests for https://github.com/AWSinAction/code3
+
+The goal of this tests is to ensure that our templates are always working. The test are implemented in Java 8 and run in JUnit 4.
+
+If you run this tests, many AWS CloudFormation tests are created and **charges will apply**!
+
+## Supported env variables
+
+* `IAM_ROLE_ARN` if the tests should assume an IAM role before they run supply the ARN of the IAM role
+* `TEMPLATE_DIR` Load templates from local disk (instead of S3 bucket `widdix-aws-cf-templates`). Must end with an `/`. See `BUCKET_NAME` as well.
+* `BUCKET_NAME` Some templates are to big to be passed as a string from local disk, therefore you need to supply the name of the bucket that is used to upload templates.
+* `BUCKET_REGION` **required if BUCKET_NAME is set** Region of the bucket
+* `DELETION_POLICY` (default `delete`, allowed values [`delete`, `retain`]) should resources be deleted?
+
+## Usage
+
+### AWS credentials
+
+The AWS credentials are passed in as defined by the AWS SDK for Java: http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html
+
+One addition is, that you can supply the env variable `IAM_ROLE_ARN` which let's the tests assume a role before they start with the default credentials.
+
+### Region selection
+
+The region selection works like defined by the AWS SDK for Java: http://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-region-selection.html
+
+### Run all tests
+
+```
+AWS_REGION="us-east-1" mvn test
+```
+
+### Run a single test suite
+
+to run the `TestJenkins` tests:
+
+```
+AWS_REGION="us-east-1" mvn -Dtest=TestJenkins test
+```
+
+### Run a single test
+
+to run the `TestJenkins.testHA` test:
+
+```
+AWS_REGION="us-east-1" mvn -Dtest=TestJenkins#testHA test
+```
+
+### Load templates from local file system
+
+```
+AWS_REGION="us-east-1" BUCKET_REGION="..." BUCKET_NAME="..." TEMPLATE_DIR="/path/to/widdix-aws-cf-templates/" mvn test
+```
+
+### Assume role
+
+This is useful if you run on a integration server like Jenkins and want to assume a different IAM role for this tests.
+
+```
+IAM_ROLE_ARN="arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME" mvn test
+```
diff --git a/test/pom.xml b/test/pom.xml
new file mode 100644
index 0000000..bbc308a
--- /dev/null
+++ b/test/pom.xml
@@ -0,0 +1,98 @@
+
+
+ 4.0.0
+
+ de.widdix
+ aws-in-action-code3-tests
+ 1.0-SNAPSHOT
+
+
+
+ com.amazonaws
+ aws-java-sdk-cloudformation
+ test
+
+
+ com.amazonaws
+ aws-java-sdk-ec2
+ test
+
+
+ com.amazonaws
+ aws-java-sdk-s3
+ test
+
+
+ com.amazonaws
+ aws-java-sdk-ecs
+ test
+
+
+ com.amazonaws
+ aws-java-sdk-sts
+ test
+
+
+ de.taimos
+ httputils
+ 1.10
+ test
+
+
+ com.evanlennick
+ retry4j
+ 0.6.2
+ test
+
+
+ com.jcraft
+ jsch
+ 0.1.54
+ test
+
+
+ junit
+ junit
+ 4.12
+ test
+
+
+
+
+
+
+ com.amazonaws
+ aws-java-sdk-bom
+ 1.11.883
+ pom
+ import
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.6.1
+
+
+ 1.8
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 2.20
+
+ classes
+ 2
+
+
+
+
+
+
diff --git a/test/src/test/java/de/widdix/awsinaction/AAWSTest.java b/test/src/test/java/de/widdix/awsinaction/AAWSTest.java
new file mode 100644
index 0000000..2183393
--- /dev/null
+++ b/test/src/test/java/de/widdix/awsinaction/AAWSTest.java
@@ -0,0 +1,123 @@
+package de.widdix.awsinaction;
+
+import com.amazonaws.auth.*;
+import com.amazonaws.regions.DefaultAwsRegionProviderChain;
+import com.amazonaws.services.s3.model.Region;
+import com.amazonaws.services.ec2.AmazonEC2;
+import com.amazonaws.services.ec2.AmazonEC2ClientBuilder;
+import com.amazonaws.services.ec2.model.*;
+import com.amazonaws.services.ec2.model.Filter;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import com.amazonaws.services.s3.model.*;
+import com.amazonaws.services.securitytoken.AWSSecurityTokenService;
+import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder;
+import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest;
+
+import java.util.List;
+import java.util.UUID;
+
+public abstract class AAWSTest extends ATest {
+
+ public final static String IAM_SESSION_NAME = "aws-in-action-code3-tests";
+
+ protected final AWSCredentialsProvider credentialsProvider;
+
+ private AmazonEC2 ec2;
+
+ private final AmazonS3 s3;
+
+ private final AWSSecurityTokenService sts;
+
+ public AAWSTest() {
+ super();
+ if (Config.has(Config.Key.IAM_ROLE_ARN)) {
+ final AWSSecurityTokenService local = AWSSecurityTokenServiceClientBuilder.standard().withCredentials(new DefaultAWSCredentialsProviderChain()).build();
+ this.credentialsProvider = new STSAssumeRoleSessionCredentialsProvider.Builder(Config.get(Config.Key.IAM_ROLE_ARN), IAM_SESSION_NAME).withStsClient(local).build();
+ } else {
+ this.credentialsProvider = new DefaultAWSCredentialsProviderChain();
+ }
+ this.ec2 = AmazonEC2ClientBuilder.standard().withCredentials(this.credentialsProvider).build();
+ this.s3 = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).build();
+ this.sts = AWSSecurityTokenServiceClientBuilder.standard().withCredentials(this.credentialsProvider).build();
+ }
+
+ protected final KeyPair createKey(final String keyName) {
+ final CreateKeyPairResult res = this.ec2.createKeyPair(new CreateKeyPairRequest().withKeyName(keyName));
+ System.out.println("keypair[" + keyName + "] created: " + res.getKeyPair().getKeyMaterial());
+ return res.getKeyPair();
+ }
+
+ protected final void deleteKey(final String keyName) {
+ if (Config.get(Config.Key.DELETION_POLICY).equals("delete")) {
+ this.ec2.deleteKeyPair(new DeleteKeyPairRequest().withKeyName(keyName));
+ System.out.println("keypair[" + keyName + "] deleted");
+ }
+ }
+
+ protected final void createBucket(final String name, final String policy) {
+ this.s3.createBucket(new CreateBucketRequest(name, Region.fromValue(this.getRegion())));
+ this.s3.setBucketPolicy(name, policy);
+ }
+
+ protected final void emptyBucket(final String name) {
+ ObjectListing objectListing = s3.listObjects(name);
+ while (true) {
+ objectListing.getObjectSummaries().forEach((summary) -> s3.deleteObject(name, summary.getKey()));
+ if (objectListing.isTruncated()) {
+ objectListing = s3.listNextBatchOfObjects(objectListing);
+ } else {
+ break;
+ }
+ }
+ VersionListing versionListing = s3.listVersions(new ListVersionsRequest().withBucketName(name));
+ while (true) {
+ versionListing.getVersionSummaries().forEach((vs) -> s3.deleteVersion(name, vs.getKey(), vs.getVersionId()));
+ if (versionListing.isTruncated()) {
+ versionListing = s3.listNextBatchOfVersions(versionListing);
+ } else {
+ break;
+ }
+ }
+ }
+
+ protected final void deleteBucket(final String name) {
+ this.emptyBucket(name);
+ this.s3.deleteBucket(new DeleteBucketRequest(name));
+ }
+
+ protected final Vpc getDefaultVPC() {
+ final DescribeVpcsResult res = this.ec2.describeVpcs(new DescribeVpcsRequest().withFilters(new Filter().withName("isDefault").withValues("true")));
+ return res.getVpcs().get(0);
+ }
+
+ protected final List getDefaultSubnets() {
+ final DescribeSubnetsResult res = this.ec2.describeSubnets(new DescribeSubnetsRequest().withFilters(new Filter().withName("defaultForAz").withValues("true")));
+ return res.getSubnets();
+ }
+
+ protected final SecurityGroup getDefaultSecurityGroup() {
+ final Vpc vpc = this.getDefaultVPC();
+ final DescribeSecurityGroupsResult res = this.ec2.describeSecurityGroups(new DescribeSecurityGroupsRequest().withFilters(
+ new Filter().withName("vpc-id").withValues(vpc.getVpcId()),
+ new Filter().withName("group-name").withValues("default")
+ ));
+ return res.getSecurityGroups().get(0);
+ }
+
+ protected final String getRegion() {
+ return new DefaultAwsRegionProviderChain().getRegion();
+ }
+
+ protected final String getAccount() {
+ return this.sts.getCallerIdentity(new GetCallerIdentityRequest()).getAccount();
+ }
+
+ protected final String random8String() {
+ final String uuid = UUID.randomUUID().toString().replace("-", "").toLowerCase();
+ final int beginIndex = (int) (Math.random() * (uuid.length() - 7));
+ final int endIndex = beginIndex + 7;
+ return "r" + uuid.substring(beginIndex, endIndex); // must begin [a-z]
+ }
+
+}
diff --git a/test/src/test/java/de/widdix/awsinaction/ACliTest.java b/test/src/test/java/de/widdix/awsinaction/ACliTest.java
new file mode 100644
index 0000000..3c460a7
--- /dev/null
+++ b/test/src/test/java/de/widdix/awsinaction/ACliTest.java
@@ -0,0 +1,79 @@
+package de.widdix.awsinaction;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSSessionCredentials;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.concurrent.TimeUnit;
+
+public abstract class ACliTest extends AAWSTest {
+
+ private String read(final InputStream is) throws Exception {
+ final BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ final StringBuilder sb = new StringBuilder();
+ String s = null;
+ while ((s = br.readLine()) != null) {
+ sb.append(s);
+ }
+ return sb.toString();
+ }
+
+ private void printCommand(final String[] command) {
+ for (final String s : command) {
+ System.out.print(s);
+ System.out.print(" ");
+ }
+ System.out.println();
+ }
+
+ protected final String exec(final String[] command) throws Exception {
+ this.printCommand(command);
+ final ProcessBuilder pb = new ProcessBuilder(command);
+ if (System.getenv().containsKey("AWS_REGION")) {
+ pb.environment().put("AWS_DEFAULT_REGION", System.getenv("AWS_REGION"));
+ }
+ if (System.getenv().containsKey("AWS_PROFILE")) {
+ pb.environment().put("AWS_DEFAULT_PROFILE", System.getenv("AWS_PROFILE"));
+ }
+ final AWSCredentials credentials = this.credentialsProvider.getCredentials();
+ pb.environment().put("AWS_ACCESS_KEY_ID", credentials.getAWSAccessKeyId());
+ pb.environment().put("AWS_SECRET_ACCESS_KEY", credentials.getAWSSecretKey());
+ if (credentials instanceof AWSSessionCredentials) {
+ pb.environment().put("AWS_SESSION_TOKEN", ((AWSSessionCredentials)credentials).getSessionToken());
+ }
+ final Process p = pb.start();
+ final String stdout = this.read(p.getInputStream());
+ final String stderr = this.read(p.getErrorStream());
+ System.out.print(stdout);
+ System.out.println();
+ if (stderr.length() > 0) {
+ System.err.print(stderr);
+ System.out.println();
+ }
+ final int exitCode = p.waitFor();
+ if (exitCode != 0) {
+ throw new Error("exec terminated with exit code: " + exitCode);
+ }
+ return stdout;
+ }
+
+ protected final void waitFor(final String expectedStdout, final String[] command) throws Exception {
+ final long start = System.currentTimeMillis();
+ final long end = start + TimeUnit.MINUTES.toMillis(60);
+ while (System.currentTimeMillis() < end) {
+ try {
+ Thread.sleep(5000);
+ } catch (final InterruptedException e) {
+ // continue
+ }
+ final String stdout = this.exec(command);
+ if (stdout.equals(expectedStdout)) {
+ return;
+ }
+ }
+ throw new RuntimeException("waitFor timed out");
+ }
+
+}
diff --git a/test/src/test/java/de/widdix/awsinaction/ACloudFormationTest.java b/test/src/test/java/de/widdix/awsinaction/ACloudFormationTest.java
new file mode 100644
index 0000000..f1e26fb
--- /dev/null
+++ b/test/src/test/java/de/widdix/awsinaction/ACloudFormationTest.java
@@ -0,0 +1,199 @@
+package de.widdix.awsinaction;
+
+import com.amazonaws.AmazonServiceException;
+import com.amazonaws.services.cloudformation.AmazonCloudFormation;
+import com.amazonaws.services.cloudformation.AmazonCloudFormationClientBuilder;
+import com.amazonaws.services.cloudformation.model.*;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.*;
+
+public abstract class ACloudFormationTest extends AAWSTest {
+
+ public static String readFile(String path, Charset encoding) {
+ try {
+ byte[] encoded = Files.readAllBytes(Paths.get(path));
+ return new String(encoded, encoding);
+ } catch (final IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private final AmazonCloudFormation cf = AmazonCloudFormationClientBuilder.standard().withCredentials(this.credentialsProvider).build();
+
+ public ACloudFormationTest() {
+ super();
+ }
+
+ protected final void createStack(final String stackName, final String template, final Parameter...parameters) {
+ CreateStackRequest req = new CreateStackRequest()
+ .withStackName(stackName)
+ .withParameters(parameters)
+ .withCapabilities(Capability.CAPABILITY_IAM, Capability.CAPABILITY_NAMED_IAM);
+ if (Config.has(Config.Key.TEMPLATE_DIR)) {
+ final String dir = Config.get(Config.Key.TEMPLATE_DIR);
+ if (Config.has(Config.Key.BUCKET_NAME)) {
+ final String bucketName = Config.get(Config.Key.BUCKET_NAME);
+ final String bucketRegion = Config.get(Config.Key.BUCKET_REGION);
+ final AmazonS3 s3local = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).withRegion(bucketRegion).build();
+ s3local.putObject(bucketName, stackName, new File(dir + template));
+ req = req.withTemplateURL("https://s3-" + bucketRegion + ".amazonaws.com/" + bucketName + "/" + stackName);
+ } else {
+ final String body = readFile(dir + template, Charset.forName("UTF-8"));
+ req = req.withTemplateBody(body);
+ }
+ } else {
+ req = req.withTemplateURL("https://s3-eu-west-1.amazonaws.com/widdix-aws-cf-templates/" + template);
+ }
+ this.cf.createStack(req);
+ this.waitForStack(stackName, FinalStatus.CREATE_COMPLETE);
+ }
+
+ protected final void updateStack(final String stackName, final String template, final Parameter...parameters) {
+ UpdateStackRequest req = new UpdateStackRequest()
+ .withStackName(stackName)
+ .withParameters(parameters)
+ .withCapabilities(Capability.CAPABILITY_IAM);
+ if (Config.has(Config.Key.TEMPLATE_DIR)) {
+ final String dir = Config.get(Config.Key.TEMPLATE_DIR);
+ if (Config.has(Config.Key.BUCKET_NAME)) {
+ final String bucketName = Config.get(Config.Key.BUCKET_NAME);
+ final String bucketRegion = Config.get(Config.Key.BUCKET_REGION);
+ final AmazonS3 s3local = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).withRegion(bucketRegion).build();
+ s3local.putObject(bucketName, stackName, new File(dir + template));
+ req = req.withTemplateURL("https://s3-" + bucketRegion + ".amazonaws.com/" + bucketName + "/" + stackName);
+ } else {
+ final String body = readFile(dir + template, Charset.forName("UTF-8"));
+ req = req.withTemplateBody(body);
+ }
+ } else {
+ req = req.withTemplateURL("https://s3-eu-west-1.amazonaws.com/widdix-aws-cf-templates/" + template);
+ }
+ this.cf.updateStack(req);
+ this.waitForStack(stackName, FinalStatus.UPDATE_COMPLETE);
+ }
+
+ protected enum FinalStatus {
+ CREATE_COMPLETE(StackStatus.CREATE_COMPLETE, false, true, StackStatus.CREATE_IN_PROGRESS),
+ DELETE_COMPLETE(StackStatus.DELETE_COMPLETE, true, false, StackStatus.DELETE_IN_PROGRESS),
+ UPDATE_COMPLETE(StackStatus.UPDATE_COMPLETE, false, false, StackStatus.UPDATE_IN_PROGRESS, StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS);
+
+ private final StackStatus finalStatus;
+ private final boolean notFoundIsFinalStatus;
+ private final boolean notFoundIsIntermediateStatus;
+ private final Set intermediateStatus;
+
+ FinalStatus(StackStatus finalStatus, boolean notFoundIsFinalStatus, boolean notFoundIsIntermediateStatus, StackStatus...intermediateStatus) {
+ this.finalStatus = finalStatus;
+ this.notFoundIsFinalStatus = notFoundIsFinalStatus;
+ this.notFoundIsIntermediateStatus = notFoundIsIntermediateStatus;
+ this.intermediateStatus = new HashSet<>(Arrays.asList(intermediateStatus));
+ }
+ }
+
+ private List getStackEvents(final String stackName) {
+ final List events = new ArrayList<>();
+ String nextToken = null;
+ do {
+ try {
+ final DescribeStackEventsResult res = this.cf.describeStackEvents(new DescribeStackEventsRequest().withStackName(stackName).withNextToken(nextToken));
+ events.addAll(res.getStackEvents());
+ nextToken = res.getNextToken();
+ } catch (final AmazonServiceException e) {
+ if (e.getErrorMessage().equals("Stack [" + stackName + "] does not exist")) {
+ nextToken = null;
+ } else {
+ throw e;
+ }
+ }
+ } while (nextToken != null);
+ Collections.reverse(events);
+ return events;
+ }
+
+ private void waitForStack(final String stackName, final FinalStatus finalStackStatus) {
+ System.out.println("waitForStack[" + stackName + "]: to reach status " + finalStackStatus.finalStatus);
+ final List eventsDisplayed = new ArrayList<>();
+ while (true) {
+ try {
+ Thread.sleep(5000);
+ } catch (final InterruptedException e) {
+ // continue
+ }
+ final List events = getStackEvents(stackName);
+ for (final StackEvent event : events) {
+ boolean displayed = false;
+ for (final StackEvent eventDisplayed : eventsDisplayed) {
+ if (event.getEventId().equals(eventDisplayed.getEventId())) {
+ displayed = true;
+ }
+ }
+ if (!displayed) {
+ System.out.println("waitForStack[" + stackName + "]: " + event.getTimestamp().toString() + " " + event.getLogicalResourceId() + " " + event.getResourceStatus() + " " + event.getResourceStatusReason());
+ eventsDisplayed.add(event);
+ }
+ }
+ try {
+ final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName));
+ final StackStatus currentStatus = StackStatus.fromValue(res.getStacks().get(0).getStackStatus());
+ if (finalStackStatus.finalStatus == currentStatus) {
+ System.out.println("waitForStack[" + stackName + "]: final status reached.");
+ return;
+ } else {
+ if (finalStackStatus.intermediateStatus.contains(currentStatus)) {
+ System.out.println("waitForStack[" + stackName + "]: continue to wait (still in intermediate status " + currentStatus + ") ...");
+ } else {
+ throw new RuntimeException("waitForStack[" + stackName + "]: reached invalid intermediate status " + currentStatus + ".");
+ }
+ }
+ } catch (final AmazonServiceException e) {
+ if (e.getErrorMessage().equals("Stack with id " + stackName + " does not exist")) {
+ if (finalStackStatus.notFoundIsFinalStatus) {
+ System.out.println("waitForStack[" + stackName + "]: final reached (not found).");
+ return;
+ } else {
+ if (finalStackStatus.notFoundIsIntermediateStatus) {
+ System.out.println("waitForStack[" + stackName + "]: continue to wait (stack not found) ...");
+ } else {
+ throw new RuntimeException("waitForStack[" + stackName + "]: stack not found.");
+ }
+ }
+ } else {
+ throw e;
+ }
+ }
+ }
+ }
+
+ protected final Map getStackOutputs(final String stackName) {
+ final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName));
+ final List