package de.widdix.awscftemplates; 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.*; import java.util.concurrent.Callable; 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); 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), UPDATE_COMPLETE(StackStatus.UPDATE_COMPLETE, false, false, StackStatus.UPDATE_COMPLETE_CLEANUP_IN_PROGRESS, StackStatus.UPDATE_IN_PROGRESS), DELETE_COMPLETE(StackStatus.DELETE_COMPLETE, true, false, StackStatus.DELETE_IN_PROGRESS); private final StackStatus finalStatus; private final boolean notFoundIsFinalStatus; private final boolean notFoundIsIntermediateStatus; private final Set<StackStatus> 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<StackEvent> getStackEvents(final String stackName) { final List<StackEvent> 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<StackEvent> eventsDisplayed = new ArrayList<>(); while (true) { try { Thread.sleep(20000); } catch (final InterruptedException e) { // continue } final List<StackEvent> 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<String, String> getStackOutputs(final String stackName) { final DescribeStacksResult res = this.cf.describeStacks(new DescribeStacksRequest().withStackName(stackName)); final List<Output> outputs = res.getStacks().get(0).getOutputs(); final Map<String, String> map = new HashMap<>(outputs.size()); for (final Output output : outputs) { map.put(output.getOutputKey(), output.getOutputValue()); } return map; } protected final String getStackOutputValue(final String stackName, final String outputKey) { return this.getStackOutputs(stackName).get(outputKey); } protected final void deleteStackAndRetryOnFailure(final String stackName) { final Callable<Boolean> callable = () -> { this.deleteStack(stackName); return true; }; this.retry(callable); } protected final void deleteStack(final String stackName) { if (Config.get(Config.Key.DELETION_POLICY).equals("delete")) { this.cf.deleteStack(new DeleteStackRequest().withStackName(stackName)); if (Config.has(Config.Key.BUCKET_NAME)) { final AmazonS3 s3local = AmazonS3ClientBuilder.standard().withCredentials(this.credentialsProvider).withRegion(Config.get(Config.Key.BUCKET_REGION)).build(); s3local.deleteObject(Config.get(Config.Key.BUCKET_NAME), stackName); } this.waitForStack(stackName, FinalStatus.DELETE_COMPLETE); } } }