com.mweagle.tereus.commands.UpdateCommand.java Source code

Java tutorial

Introduction

Here is the source code for com.mweagle.tereus.commands.UpdateCommand.java

Source

// Copyright (c) 2015 Matt Weagle (mweagle@gmail.com)

// Permission is hereby granted, free of charge, to
// any person obtaining a copy of this software and
// associated documentation files (the "Software"),
// to deal in the Software without restriction,
// including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so,
// subject to the following conditions:

// The above copyright notice and this permission
// notice shall be included in all copies or substantial
// portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
package com.mweagle.tereus.commands;

import java.io.ByteArrayInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.logging.log4j.LogManager;

import com.amazonaws.services.cloudformation.AmazonCloudFormationAsyncClient;
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
import com.amazonaws.services.cloudformation.model.DescribeStacksResult;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.Stack;
import com.amazonaws.services.cloudformation.model.UpdateStackRequest;
import com.google.common.base.Preconditions;
import com.google.gson.GsonBuilder;
import com.mweagle.tereus.CONSTANTS;
import com.mweagle.tereus.aws.CloudFormation;
import com.mweagle.tereus.aws.S3Resource;
import com.mweagle.tereus.commands.pipelines.UpdatePipeline;
import com.mweagle.tereus.input.UpdateInput;

import io.airlift.airline.Command;
import io.airlift.airline.Option;

@Command(name = "update", description = "Update a CloudFormation stack by name or ID via a JSON Patch (RFC 6902)")
public class UpdateCommand extends AbstractTereusAWSCommand {
    @Option(name = { "-p",
            "--patch" }, description = "Path to CloudFormation patch definition file [REQUIRED]", required = true)
    public String patchDefinitionPath;

    @Option(name = { "-a",
            "--arg" }, arity = 2, description = "Name-value argument pair. Published as ARGUMENTS in JSON Patch evaluation")
    public List<String> arguments;

    @Option(name = { "-o", "--output" }, description = "Optional file to which evaluated template will be saved")
    public String outputFilePath;

    @edu.umd.cs.findbugs.annotations.SuppressFBWarnings({ "DM_EXIT", "OBL_UNSATISFIED_OBLIGATION" })
    @Override
    public void run() {
        Map<String, Object> argumentMap = new HashMap<>();
        Optional<OutputStream> osSink = Optional.empty();
        int exitCode = 0;
        final UpdateInput updateInput = new UpdateInput(this.patchDefinitionPath, argumentMap, this.region,
                this.dryRun);
        try {
            if (null != this.outputFilePath) {
                final Path outputPath = Paths.get(this.outputFilePath);
                osSink = Optional.of(new FileOutputStream(outputPath.toFile()));
            }
            this.update(updateInput, osSink);
        } catch (Exception ex) {
            LogManager.getLogger().error(ex);
            exitCode = 1;
        } finally {
            if (osSink.isPresent()) {
                try {
                    osSink.get().close();
                } catch (Exception e) {
                    // NOP
                }
            }
        }
        System.exit(exitCode);
    }

    public Map<String, Object> update(final UpdateInput input, Optional<? extends OutputStream> osSinkTemplate)
            throws Exception {
        final UpdatePipeline pipeline = new UpdatePipeline(input.patchPath, input.arguments, input.awsCredentials,
                input.awsRegion, input.dryRun, input.logger);

        Map<String, Object> evaluationResult = pipeline.run(input.patchPath, input.logger);
        final Optional<Object> patchData = Optional.ofNullable(evaluationResult.get("Patch"));
        if (osSinkTemplate.isPresent()) {
            final String formattedTemplate = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()
                    .toJson(patchData.get());
            osSinkTemplate.get().write(formattedTemplate.getBytes(Charset.forName("UTF-8")));
        }
        updateStack(input, evaluationResult.get("Result").toString(), evaluationResult.get("StackName").toString());

        // Wait
        return evaluationResult;
    }

    protected Optional<Parameter> findNamedParameter(final String paramName, final List<Parameter> params) {
        return params.stream().filter(eachParameter -> (0 == eachParameter.getParameterKey().compareTo(paramName)))
                .findFirst();
    }

    protected void updateStack(UpdateInput updateInput, String transformedTemplate, String stackTargetName)
            throws UnsupportedEncodingException {
        if (updateInput.dryRun) {
            updateInput.logger.info("Dry run requested (-n/--noop). Stack update bypassed.");
        } else {
            // Fetch the stack parameters
            final DescribeStacksRequest stackRequest = new DescribeStacksRequest().withStackName(stackTargetName);
            final AmazonCloudFormationAsyncClient awsClient = new AmazonCloudFormationAsyncClient(
                    updateInput.awsCredentials);
            awsClient.setRegion(updateInput.awsRegion);
            final DescribeStacksResult result = awsClient.describeStacks(stackRequest);
            final Stack existantStack = result.getStacks().get(0);

            final Optional<Parameter> s3Bucket = findNamedParameter(CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME,
                    existantStack.getParameters());

            Preconditions.checkArgument(s3Bucket.isPresent(),
                    "Failed to determine S3 BucketName from existant template via parameter name: "
                            + CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME);

            // Super, now put the new content to S3, update the parameter list
            // to include the new URL, and submit the updated stack.
            final byte[] templateBytes = transformedTemplate.getBytes("UTF-8");
            final InputStream is = new ByteArrayInputStream(templateBytes);
            final String templateDigest = DigestUtils.sha256Hex(templateBytes);
            final String keyName = String.format("%s-tereus.cf.template", templateDigest);

            try (S3Resource resource = new S3Resource(s3Bucket.get().getParameterValue(), keyName, is,
                    Optional.of(Long.valueOf(templateBytes.length)))) {
                // Upload the template
                resource.upload();

                // Go ahead and create the stack.
                final UpdateStackRequest request = new UpdateStackRequest().withStackName(stackTargetName);
                request.setTemplateURL(resource.getResourceURL().get());
                request.setParameters(existantStack.getParameters());
                request.setCapabilities(Arrays.asList("CAPABILITY_IAM"));

                updateInput.logger.debug("Updating stack: {}", stackTargetName);
                final Optional<DescribeStacksResult> updateResult = new CloudFormation().updateStack(request,
                        updateInput.awsRegion, updateInput.logger);

                // If everything worked out, then release the template
                // URL s.t. subsequent ASG instantiated instances have access
                // to the template content
                if (updateResult.isPresent()) {
                    updateInput.logger.info("Stack successfully updated");
                    updateInput.logger.info(updateResult.get().toString());
                    resource.setReleased(true);
                }
            }
        }
    }
}