com.nike.cerberus.service.CloudFormationService.java Source code

Java tutorial

Introduction

Here is the source code for com.nike.cerberus.service.CloudFormationService.java

Source

/*
 * Copyright (c) 2016 Nike, Inc.
 *
 * 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 com.nike.cerberus.service;

import com.amazonaws.AmazonServiceException;
import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.CreateStackRequest;
import com.amazonaws.services.cloudformation.model.CreateStackResult;
import com.amazonaws.services.cloudformation.model.DeleteStackRequest;
import com.amazonaws.services.cloudformation.model.DescribeStackEventsRequest;
import com.amazonaws.services.cloudformation.model.DescribeStackEventsResult;
import com.amazonaws.services.cloudformation.model.DescribeStacksRequest;
import com.amazonaws.services.cloudformation.model.DescribeStacksResult;
import com.amazonaws.services.cloudformation.model.Output;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.StackEvent;
import com.amazonaws.services.cloudformation.model.StackStatus;
import com.amazonaws.services.cloudformation.model.UpdateStackRequest;
import com.beust.jcommander.internal.Maps;
import com.github.tomaslanger.chalk.Chalk;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.nike.cerberus.ConfigConstants;
import com.nike.cerberus.domain.EnvironmentMetadata;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;
import javax.inject.Inject;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

/**
 * Service wrapper for AWS CloudFormation.
 */
public class CloudFormationService {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final AmazonCloudFormation cloudFormationClient;
    private final EnvironmentMetadata environmentMetadata;

    @Inject
    public CloudFormationService(final AmazonCloudFormation cloudFormationClient,
            final EnvironmentMetadata environmentMetadata) {
        this.cloudFormationClient = cloudFormationClient;
        this.environmentMetadata = environmentMetadata;
    }

    /**
     * Creates a new stack.
     *
     * @param name Stack name.
     * @param parameters Input parameters.
     * @param templatePath Classpath to the JSON template of the stack.
     * @return Stack ID
     */
    public String createStack(final String name, final Map<String, String> parameters, final String templatePath,
            final boolean iamCapabilities) {
        logger.info(String.format("Executing the Cloud Formation: %s, Stack Name: %s", templatePath, name));

        final CreateStackRequest request = new CreateStackRequest().withStackName(name)
                .withParameters(convertParameters(parameters)).withTemplateBody(getTemplateText(templatePath));

        if (iamCapabilities) {
            request.getCapabilities().add("CAPABILITY_IAM");
        }

        final CreateStackResult result = cloudFormationClient.createStack(request);
        return result.getStackId();
    }

    /**
     * Updates an existing stack by name.
     *
     * @param stackId
     * @param parameters
     * @param iamCapabilities
     */
    public void updateStack(final String stackId, final Map<String, String> parameters,
            final boolean iamCapabilities) {
        updateStack(stackId, parameters, null, iamCapabilities);
    }

    /**
     * Updates an existing stack by name.
     *
     * @param stackId Stack ID.
     * @param parameters Input parameters.
     * @param templatePath Path to the JSON template of the stack.
     */
    public void updateStack(final String stackId, final Map<String, String> parameters, final String templatePath,
            final boolean iamCapabilities) {
        final UpdateStackRequest request = new UpdateStackRequest().withStackName(stackId)
                .withParameters(convertParameters(parameters));

        if (StringUtils.isNotBlank(templatePath)) {
            request.withTemplateBody(getTemplateText(templatePath));
        } else {
            request.withUsePreviousTemplate(true);
        }

        if (iamCapabilities) {
            request.getCapabilities().add("CAPABILITY_IAM");
        }

        cloudFormationClient.updateStack(request);
    }

    /**
     * Deletes an existing stack by name.
     *
     * @param stackId Stack ID.
     */
    public void deleteStack(final String stackId) {
        final DeleteStackRequest request = new DeleteStackRequest().withStackName(stackId);
        cloudFormationClient.deleteStack(request);
    }

    /**
     * Returns the current status of the named stack.
     *
     * @param stackId Stack ID.
     * @return Stack status data.
     */
    @Nullable
    public StackStatus getStackStatus(final String stackId) {
        final DescribeStacksRequest request = new DescribeStacksRequest().withStackName(stackId);

        try {
            final DescribeStacksResult result = cloudFormationClient.describeStacks(request);

            if (result.getStacks().size() > 0) {
                final String status = result.getStacks().get(0).getStackStatus();

                if (StringUtils.isNotBlank(status)) {
                    return StackStatus.fromValue(status);
                }
            }
        } catch (final AmazonServiceException ase) {
            // Stack doesn't exist, just return with no status
            if (ase.getStatusCode() != 400) {
                throw ase;
            }
        }

        return null;
    }

    /**
     * Returns the current status of the named stack.
     *
     * @param stackId Stack name.
     * @return Stack outputs data.
     */
    public Map<String, String> getStackParameters(final String stackId) {
        final DescribeStacksRequest request = new DescribeStacksRequest().withStackName(stackId);
        final DescribeStacksResult result = cloudFormationClient.describeStacks(request);
        final Map<String, String> parameters = Maps.newHashMap();

        if (result.getStacks().size() > 0) {
            parameters.putAll(result.getStacks().get(0).getParameters().stream()
                    .collect(Collectors.toMap(Parameter::getParameterKey, Parameter::getParameterValue)));

        }

        return parameters;
    }

    /**
     * Returns the current status of the named stack.
     *
     * @param stackId Stack name.
     * @return Stack outputs data.
     */
    public Map<String, String> getStackOutputs(final String stackId) {
        final DescribeStacksRequest request = new DescribeStacksRequest().withStackName(stackId);
        final DescribeStacksResult result = cloudFormationClient.describeStacks(request);
        final Map<String, String> outputs = Maps.newHashMap();

        if (result.getStacks().size() > 0) {
            outputs.putAll(result.getStacks().get(0).getOutputs().stream()
                    .collect(Collectors.toMap(Output::getOutputKey, Output::getOutputValue)));

        }

        return outputs;
    }

    /**
     * Returns the events for a named stack.
     *
     * @param stackId Stack ID.
     * @return Collection of events
     */
    public List<StackEvent> getStackEvents(final String stackId) {
        final DescribeStackEventsRequest request = new DescribeStackEventsRequest().withStackName(stackId);

        try {
            final DescribeStackEventsResult result = cloudFormationClient.describeStackEvents(request);
            return result.getStackEvents();
        } catch (final AmazonServiceException ase) {
            // Stack doesn't exist, just return with no status
            if (ase.getStatusCode() != 400) {
                throw ase;
            }
        }

        return Collections.emptyList();
    }

    /**
     * Checks if a stack exists with the specified ID.
     *
     * @param stackId Stack ID.
     * @return boolean
     */
    public boolean isStackPresent(final String stackId) {
        Preconditions.checkArgument(StringUtils.isNotBlank(stackId), "Stack ID can not be blank");

        return getStackStatus(stackId) != null;
    }

    /**
     * Converts a Map into a list of {@link com.amazonaws.services.cloudformation.model.Parameter} objects.
     *
     * @param parameterMap Map to be converted.
     * @return Collection of parameters.
     */
    public Collection<Parameter> convertParameters(final Map<String, String> parameterMap) {
        final List<Parameter> parameterList = Lists.newArrayListWithCapacity(parameterMap.size());

        for (Map.Entry<String, String> entry : parameterMap.entrySet()) {
            final Parameter param = new Parameter().withParameterKey(entry.getKey())
                    .withParameterValue(entry.getValue());
            parameterList.add(param);
        }

        return parameterList;
    }

    /**
     * Gets the template contents from the file on the classpath.
     *
     * @param templatePath Classpath for the template to be read
     * @return Template contents
     */
    public String getTemplateText(final String templatePath) {
        final InputStream templateStream = getClass().getResourceAsStream(templatePath);

        if (templateStream == null) {
            throw new IllegalStateException(String.format(
                    "The CloudFormation JSON template doesn't exist on the classpath. path: %s", templatePath));
        }

        try {
            return IOUtils.toString(templateStream, ConfigConstants.DEFAULT_ENCODING);
        } catch (final IOException e) {
            final String errorMessage = String.format("Unable to read input stream from %s", templatePath);
            logger.error(errorMessage);
            throw new RuntimeException(errorMessage, e);
        }
    }

    /**
     * Prepends the name with the environment name. ([environment]-[name])
     *
     * @param name Custom stack name
     * @return Formatted name with environment prepended
     */
    public String getEnvStackName(final String name) {
        return String.format("%s-%s", environmentMetadata.getName(), name);
    }

    /**
     * Blocking call that waits for a stack change to complete.  Times out if waiting more than 30 minutes.
     *
     * @param endStatuses Status to end on
     * @return The final status
     */
    public StackStatus waitForStatus(final String stackId, final HashSet<StackStatus> endStatuses) {
        DateTime now = DateTime.now(DateTimeZone.UTC).minusSeconds(10);
        DateTime timeoutDateTime = now.plusMinutes(90);
        Set<String> recordedStackEvents = Sets.newHashSet();
        boolean isRunning = true;
        StackStatus stackStatus = null;

        while (isRunning) {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                logger.warn("Thread sleep interrupted. Continuing...", e);
            }
            stackStatus = getStackStatus(stackId);

            if (endStatuses.contains(stackStatus) || stackStatus == null) {
                isRunning = false;
            }

            if (stackStatus != null) {
                List<StackEvent> stackEvents = getStackEvents(stackId);
                stackEvents.sort((o1, o2) -> o1.getTimestamp().compareTo(o2.getTimestamp()));

                for (StackEvent stackEvent : stackEvents) {
                    DateTime eventTime = new DateTime(stackEvent.getTimestamp());
                    if (!recordedStackEvents.contains(stackEvent.getEventId()) && now.isBefore(eventTime)) {
                        logger.info(String.format("TS: %s, Status: %s, Type: %s, Reason: %s",
                                Chalk.on(stackEvent.getTimestamp().toString()).yellow(),
                                getStatusColor(stackEvent.getResourceStatus()),
                                Chalk.on(stackEvent.getResourceType()).yellow(),
                                Chalk.on(stackEvent.getResourceStatusReason()).yellow()));

                        recordedStackEvents.add(stackEvent.getEventId());
                    }
                }
            }

            if (timeoutDateTime.isBeforeNow()) {
                logger.error("Timed out waiting for CloudFormation completion status.");
                isRunning = false;
            }
        }

        return stackStatus;
    }

    private String getStatusColor(String status) {
        if (status.endsWith("PROGRESS")) {
            return Chalk.on(status).yellow().toString();
        } else if (status.endsWith("COMPLETE")) {
            return Chalk.on(status).green().bold().toString();
        } else if (status.endsWith("FAILED")) {
            return Chalk.on(status).red().bold().toString();
        }
        return status;
    }
}