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

Java tutorial

Introduction

Here is the source code for com.mweagle.tereus.commands.CreateCommand.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.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

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

import com.amazonaws.services.cloudformation.AmazonCloudFormationClient;
import com.amazonaws.services.cloudformation.model.CreateStackRequest;
import com.amazonaws.services.cloudformation.model.DescribeStacksResult;
import com.amazonaws.services.cloudformation.model.EstimateTemplateCostRequest;
import com.amazonaws.services.cloudformation.model.EstimateTemplateCostResult;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.Tag;
import com.amazonaws.services.cloudformation.model.ValidateTemplateRequest;
import com.amazonaws.services.cloudformation.model.ValidateTemplateResult;
import com.google.common.base.Charsets;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.mweagle.tereus.CONSTANTS;
import com.mweagle.tereus.aws.CloudFormation;
import com.mweagle.tereus.aws.S3Resource;
import com.mweagle.tereus.commands.pipelines.CreationPipeline;
import com.mweagle.tereus.input.TereusInput;

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

@Command(name = "create", description = "Create a CloudFormation stack")
public class CreateCommand extends AbstractTereusAWSCommand {
    @Option(name = { "-t",
            "--template" }, description = "Path to CloudFormation definition [REQUIRED]", required = true)
    public String stackTemplatePath;

    @Option(name = { "-a",
            "--arguments" }, description = "Path to JSON file including \"Parameters\" & \"Tags\" values")
    public String jsonParamAndTagsPath;

    @Option(name = { "-b",
            "--bucket" }, description = "S3 Bucketname to host stack resources. MUST be CLI option OR `Parameters.BucketName` value in JSON input")
    public String s3BucketName;

    @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" })
    @SuppressWarnings("unchecked")
    @Override
    public void run() {
        int exitCode = 0;
        try {
            final String argumentJSON = (null != this.jsonParamAndTagsPath)
                    ? new String(Files.readAllBytes(Paths.get(this.jsonParamAndTagsPath)), Charsets.UTF_8)
                    : null;

            Map<String, Object> jsonJavaRootObject = (null != argumentJSON)
                    ? new Gson().fromJson(argumentJSON, Map.class)
                    : Collections.emptyMap();
            Map<String, Object> parameters = (Map<String, Object>) jsonJavaRootObject
                    .getOrDefault(CONSTANTS.ARGUMENT_JSON_KEYNAMES.PARAMETERS, new HashMap<>());
            final String jsonS3BucketName = ((String) parameters
                    .getOrDefault(CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME, "")).trim();
            final String cliS3BucketName = (null == this.s3BucketName) ? "" : this.s3BucketName.trim();

            if (!jsonS3BucketName.isEmpty() && !cliS3BucketName.isEmpty()) {
                final String msg = String.format("S3 bucketname defined in both %s and via command line argument",
                        this.jsonParamAndTagsPath);
                throw new IllegalArgumentException(msg);
            } else if (!cliS3BucketName.isEmpty()) {
                parameters.put(CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME, cliS3BucketName);
            }

            Map<String, Object> tags = (Map<String, Object>) jsonJavaRootObject
                    .getOrDefault(CONSTANTS.ARGUMENT_JSON_KEYNAMES.TAGS, Collections.emptyMap());

            TereusInput tereusInput = new TereusInput(this.stackTemplatePath, this.region, parameters, tags,
                    this.dryRun);

            Optional<OutputStream> osSink = Optional.empty();
            try {
                if (null != this.outputFilePath) {
                    final Path outputPath = Paths.get(this.outputFilePath);
                    osSink = Optional.of(new FileOutputStream(outputPath.toFile()));
                }
                this.create(tereusInput, osSink);
            } catch (Exception ex) {
                LogManager.getLogger().error(ex.getCause());
                exitCode = 2;
            } finally {
                if (osSink.isPresent()) {
                    try {
                        osSink.get().close();
                    } catch (Exception e) {
                        // NOP
                    }
                }
            }
        } catch (Exception ex) {
            LogManager.getLogger().error(ex);
            Help.help(this.helpOption.commandMetadata);
            exitCode = 1;
        }
        System.exit(exitCode);
    }

    public Map<String, Object> create(final TereusInput tereusInput,
            Optional<? extends OutputStream> osSinkTemplate) throws Exception {
        final CreationPipeline pipeline = new CreationPipeline(tereusInput);
        Map<String, Object> evaluationResult = pipeline.run(tereusInput.stackDefinitionPath, tereusInput.logger);

        final Optional<Object> templateData = Optional.ofNullable(evaluationResult.get("Template"));
        final Optional<Object> prepopulatedTemplate = Optional
                .ofNullable(evaluationResult.get("ParameterizedTemplate"));
        final Optional<String> stackName = Optional.of((String) evaluationResult.get("StackName"));

        // Upload the parameterized template to S3, validate it, and cleanup
        validateTemplate(tereusInput,
                new GsonBuilder().disableHtmlEscaping().create().toJson(prepopulatedTemplate.get()));

        // Create the stack
        createStack(stackName, tereusInput, (JsonElement) templateData.get(), !osSinkTemplate.isPresent());

        if (osSinkTemplate.isPresent()) {
            final String formattedTemplate = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()
                    .toJson(templateData.get());
            osSinkTemplate.get().write(formattedTemplate.getBytes(Charset.forName("UTF-8")));
        }
        return evaluationResult;
    }

    protected List<Parameter> toParameterList(final Map<String, Object> values) {
        return values.entrySet().stream().map(eachEntry -> {
            Parameter awsParam = new Parameter();
            awsParam.setParameterKey(eachEntry.getKey());
            awsParam.setParameterValue(eachEntry.getValue().toString());
            return awsParam;
        }).collect(Collectors.toList());
    }

    protected List<Tag> toTagList(final Map<String, Object> values) {
        List<Tag> creationTags = values.entrySet().stream().map(eachEntry -> {
            Tag awsTag = new Tag();
            awsTag.setKey(eachEntry.getKey());
            awsTag.setValue(eachEntry.getValue().toString());
            return awsTag;
        }).collect(Collectors.toList());

        // Add the version tag
        // TODO - semver enforcement on updates
        final Tag versionTag = new Tag().withKey(String.format("%s:version", CONSTANTS.TEREUS_TAG_NAMESPACE))
                .withValue(CONSTANTS.TEREUS_VERSION);
        creationTags.add(versionTag);
        return creationTags;
    }

    protected void createStack(Optional<String> stackName, TereusInput tereusInput, JsonElement templateData,
            boolean logTemplate) throws UnsupportedEncodingException {
        if (tereusInput.dryRun) {
            tereusInput.logger.info("Dry run requested (-n/--noop). Stack creation bypassed.");
            if (logTemplate) {
                final String formattedTemplate = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping()
                        .create().toJson(templateData);
                tereusInput.logger.info("Stack Template:\n {}", formattedTemplate);
            }
        } else {
            final String bucketName = tereusInput.params.get(CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME).toString();
            // Upload the template
            final String templateContent = new GsonBuilder().create().toJson(templateData);
            final byte[] templateBytes = templateContent.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(bucketName, keyName, is,
                    Optional.of(Long.valueOf(templateBytes.length)))) {
                resource.upload();
                final EstimateTemplateCostRequest costRequest = new EstimateTemplateCostRequest();
                costRequest.setParameters(toParameterList(tereusInput.params));
                costRequest.setTemplateURL(resource.getResourceURL().get());
                final AmazonCloudFormationClient awsClient = new AmazonCloudFormationClient(
                        tereusInput.awsCredentials);
                awsClient.setRegion(tereusInput.awsRegion);
                final EstimateTemplateCostResult costResult = awsClient.estimateTemplateCost(costRequest);
                tereusInput.logger.info("Cost Estimator: {}", costResult.getUrl());

                // Go ahead and create the stack.
                final String defaultTemplateName = String.format("Tereus-%s", System.currentTimeMillis());
                final CreateStackRequest request = new CreateStackRequest()
                        .withStackName(stackName.orElse(defaultTemplateName))
                        .withTemplateURL(resource.getResourceURL().get())
                        .withParameters(toParameterList(tereusInput.params)).withTags(toTagList(tereusInput.tags))
                        .withCapabilities("CAPABILITY_IAM");
                tereusInput.logger.debug("Creating stack: {}", stackName);
                tereusInput.logger.debug("Stack params: {}", request.getParameters());
                tereusInput.logger.debug("Stack tags: {}", request.getTags());
                final Optional<DescribeStacksResult> result = new CloudFormation().createStack(request,
                        tereusInput.awsRegion, tereusInput.logger);
                if (result.isPresent()) {
                    tereusInput.logger.info("Stack successfully created");
                    tereusInput.logger.info(result.get().toString());
                    resource.setReleased(true);
                }
            }
        }
    }

    protected void validateTemplate(TereusInput tereusInput, String parameterizedTemplate)
            throws UnsupportedEncodingException {
        if (tereusInput.dryRun) {
            tereusInput.logger.info("Dry run requested (-n/--noop). Stack validation bypassed.");
        } else {
            tereusInput.logger.info("Validating template with AWS");
            final String bucketName = tereusInput.params.get(CONSTANTS.PARAMETER_NAMES.S3_BUCKET_NAME).toString();
            final byte[] templateBytes = parameterizedTemplate.getBytes("UTF-8");
            final InputStream is = new ByteArrayInputStream(templateBytes);

            final String templateDigest = DigestUtils.sha256Hex(templateBytes);
            final String keyName = String.format("%s-tereus-pre.cf.template", templateDigest);
            try (S3Resource resource = new S3Resource(bucketName, keyName, is,
                    Optional.of(Long.valueOf(templateBytes.length)))) {
                Optional<String> templateURL = resource.upload();
                final ValidateTemplateRequest validationRequest = new ValidateTemplateRequest();
                validationRequest.setTemplateURL(templateURL.get());
                final AmazonCloudFormationClient awsClient = new AmazonCloudFormationClient(
                        tereusInput.awsCredentials);
                awsClient.setRegion(tereusInput.awsRegion);
                final ValidateTemplateResult validationResult = awsClient.validateTemplate(validationRequest);
                tereusInput.logger.debug("Stack template validation results:");
                tereusInput.logger.debug(validationResult.toString());
            }
        }
    }
}