/* * Copyright (c) 2016 ingenieux Labs * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License 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 br.com.ingenieux.mojo.cloudformation; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.cloudformation.model.Capability; import com.amazonaws.services.cloudformation.model.CreateStackRequest; import com.amazonaws.services.cloudformation.model.CreateStackResult; import com.amazonaws.services.cloudformation.model.OnFailure; import com.amazonaws.services.cloudformation.model.StackStatus; import com.amazonaws.services.cloudformation.model.Tag; import com.amazonaws.services.cloudformation.model.UpdateStackRequest; import com.amazonaws.services.cloudformation.model.UpdateStackResult; import com.amazonaws.services.cloudformation.model.ValidateTemplateRequest; import com.amazonaws.services.cloudformation.model.ValidateTemplateResult; import com.amazonaws.services.s3.AmazonS3URI; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrSubstitutor; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import java.io.File; import java.io.FileInputStream; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import br.com.ingenieux.mojo.aws.util.BeanstalkerS3Client; import br.com.ingenieux.mojo.cloudformation.cmd.WaitForStackCommand; import static java.util.Arrays.asList; import static org.apache.commons.lang.StringUtils.isNotBlank; /** * Pushes (creates/updates) a given stack */ @Mojo(name = "push-stack") public class PushStackMojo extends AbstractCloudformationMojo { @Parameter( property = "cloudformation.stackLocation", required = true, defaultValue = "${project.basedir}/src/main/cloudformation/${project.artifactId}.template.json" ) File templateLocation; /** * <p> S3 URL "s3://<bucketName>/<keyPath>" of the S3 Location </p> * * <p>If set, will upload the @{templateLocation} contents prior to issuing a stack * create/update process</p> */ @Parameter(property = "cloudformation.s3Url") String s3Url; AmazonS3URI destinationS3Uri; /** * <p>Template Input Parameters</p> * * <p>On CLI usage, you can set <code>-Dcloudformation.paramters=ParamA=abc,ParamB=def</code></p> */ @Parameter(property = "cloudformation.parameters") List<com.amazonaws.services.cloudformation.model.Parameter> parameters = new ArrayList<>(); public void setParameters(String parameters) { List<String> nvPairs = asList(parameters.split(",")); this.parameters = nvPairs .stream() .map(this::extractNVPair) .map( mapEntry -> new com.amazonaws.services.cloudformation.model.Parameter().withParameterKey(mapEntry.getKey()).withParameterValue(mapEntry.getValue())) .collect(Collectors.toList()); } /** * Notification ARNs */ @Parameter List<String> notificationArns; /** * <p>On Failure</p> * * <p>Either "DO_NOTHING", "ROLLBACK" or "DELETE"</p> */ @Parameter(property = "cloudformation.onfailure", defaultValue = "DO_NOTHING") OnFailure onFailure = OnFailure.DO_NOTHING; /** * Resource Types */ @Parameter Collection<String> resourceTypes = new ArrayList<>(); /** * Disable Rollback? */ @Parameter(defaultValue = "true") Boolean disableRollback = true; /** * Tags */ @Parameter(property = "cloudformation.tags") List<Tag> tags = new ArrayList<>(); public void setTags(String tags) { List<String> nvPairs = asList(tags.split(",")); this.tags = nvPairs .stream() .map(this::extractNVPair) .map(mapEntry -> new Tag().withKey(mapEntry.getKey()).withValue(mapEntry.getValue())) .collect(Collectors.toList()); } /** * Timeout in Minutes */ @Parameter(property = "cloudformation.timeoutInMinutes") Integer timeoutInMinutes; BeanstalkerS3Client s3Client; private String templateBody; @Override protected Object executeInternal() throws Exception { shouldFailIfMissingStack(false); if (!templateLocation.exists() && !templateLocation.isFile()) { getLog().warn("File not found (or not a file): " + templateLocation.getPath() + ". Skipping."); return null; } if (isNotBlank(s3Url)) { getLog().info("Uploading file " + this.templateLocation + " to location " + this.s3Url); s3Client = new BeanstalkerS3Client(getAWSCredentials(), getClientConfiguration(), getRegion()); s3Client.setMultipartUpload(false); this.destinationS3Uri = new AmazonS3URI(s3Url); uploadContents(templateLocation, destinationS3Uri); } else { templateBody = IOUtils.toString(new FileInputStream(this.templateLocation)); final Properties props = getProperties(); templateBody = new StrSubstitutor(new StrLookup<String>() { @Override public String lookup(String key) { return (String) props.get(key); } }).replace(templateBody); } { ValidateTemplateResult validateTemplateResult = validateTemplate(); if (!validateTemplateResult.getParameters().isEmpty()) { Set<String> existingParameterNames = this.parameters.stream().map(x -> x.getParameterKey()).collect(Collectors.toSet()); Set<String> requiredParameterNames = validateTemplateResult.getParameters().stream().map(x -> x.getParameterKey()).collect(Collectors.toSet()); for (String requiredParameter : requiredParameterNames) { if (!existingParameterNames.contains(requiredParameter)) { getLog().warn("Missing required parameter name: " + requiredParameter); getLog().warn("If its an update, will reuse previous value"); } this.parameters.add(new com.amazonaws.services.cloudformation.model.Parameter().withParameterKey(requiredParameter).withUsePreviousValue(true)); } } } WaitForStackCommand.WaitForStackContext ctx = null; Object result = null; if (null == stackSummary) { getLog().info("Must Create Stack"); CreateStackResult createStackResult; result = createStackResult = createStack(); ctx = new WaitForStackCommand.WaitForStackContext(createStackResult.getStackId(), getService(), this::info, 30, asList(StackStatus.CREATE_COMPLETE)); } else { getLog().info("Must Update Stack"); UpdateStackResult updateStackResult; result = updateStackResult = updateStack(); if (null != result) { ctx = new WaitForStackCommand.WaitForStackContext(updateStackResult.getStackId(), getService(), this::info, 30, asList(StackStatus.UPDATE_COMPLETE)); } } if (null != ctx) new WaitForStackCommand(ctx).execute(); return result; } private ValidateTemplateResult validateTemplate() throws Exception { final ValidateTemplateRequest req = new ValidateTemplateRequest(); if (null != destinationS3Uri) { req.withTemplateURL(generateExternalUrl(this.destinationS3Uri)); } else { req.withTemplateBody(templateBody); } final ValidateTemplateResult result = getService().validateTemplate(req); getLog().info("Validation Result: " + result); return result; } private CreateStackResult createStack() throws Exception { CreateStackRequest req = new CreateStackRequest().withStackName(stackName).withCapabilities(Capability.CAPABILITY_IAM); if (null != this.destinationS3Uri) { req.withTemplateURL(generateExternalUrl(this.destinationS3Uri)); } else { req.withTemplateBody(templateBody); } req.withNotificationARNs(notificationArns); req.withParameters(parameters); req.withResourceTypes(resourceTypes); req.withDisableRollback(disableRollback); req.withTags(tags); req.withTimeoutInMinutes(timeoutInMinutes); return getService().createStack(req); } protected String generateExternalUrl(AmazonS3URI destinationS3Uri) throws Exception { return s3Client.getResourceUrl(destinationS3Uri.getBucket(), destinationS3Uri.getKey()); } private UpdateStackResult updateStack() throws Exception { UpdateStackRequest req = new UpdateStackRequest().withStackName(stackName).withCapabilities(Capability.CAPABILITY_IAM); if (null != this.destinationS3Uri) { req.withTemplateURL(generateExternalUrl(this.destinationS3Uri)); } else { req.withTemplateBody(templateBody); } req.withNotificationARNs(notificationArns); req.withParameters(parameters); req.withResourceTypes(resourceTypes); req.withTags(tags); try { return getService().updateStack(req); } catch (AmazonServiceException exc) { if ("No updates are to be performed.".equals(exc.getErrorMessage())) { return null; } throw exc; } } private void uploadContents(File templateLocation, AmazonS3URI destinationS3Uri) throws Exception { s3Client.putObject(destinationS3Uri.getBucket(), destinationS3Uri.getKey(), new FileInputStream(this.templateLocation), null); } }