StackSetInstanceManager.java

/*
 * This file is part of the pl.wrzasq.lambda.
 *
 * @license http://mit-license.org/ The MIT license
 * @copyright 2019 © by Rafał Wrzeszcz - Wrzasq.pl.
 */

package pl.wrzasq.lambda.cform.stackset.instance.service;

import java.util.Collection;

import com.amazonaws.services.cloudformation.AmazonCloudFormation;
import com.amazonaws.services.cloudformation.model.CreateStackInstancesRequest;
import com.amazonaws.services.cloudformation.model.DeleteStackInstancesRequest;
import com.amazonaws.services.cloudformation.model.DescribeStackInstanceRequest;
import com.amazonaws.services.cloudformation.model.Parameter;
import com.amazonaws.services.cloudformation.model.StackInstance;
import com.amazonaws.services.cloudformation.model.StackInstanceNotFoundException;
import com.amazonaws.services.cloudformation.model.UpdateStackInstancesRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pl.wrzasq.commons.aws.cloudformation.CustomResourceResponse;
import pl.wrzasq.commons.aws.cloudformation.StackSetHandler;
import pl.wrzasq.commons.aws.cloudformation.StackUtils;
import pl.wrzasq.lambda.cform.stackset.instance.model.StackInstanceRequest;

/**
 * CloudFormation API implementation.
 */
public class StackSetInstanceManager {
    /**
     * Logger.
     */
    private Logger logger = LoggerFactory.getLogger(StackSetInstanceManager.class);

    /**
     * AWS CloudFormation API client.
     */
    private AmazonCloudFormation cloudFormation;

    /**
     * Stack set operations helper.
     */
    private StackSetHandler stackSetHandler;

    /**
     * Initializes object with given CloudFormation client.
     *
     * @param cloudFormation AWS CloudFormation client.
     * @param stackSetHandler Stack set operations helper.
     */
    public StackSetInstanceManager(AmazonCloudFormation cloudFormation, StackSetHandler stackSetHandler) {
        this.cloudFormation = cloudFormation;
        this.stackSetHandler = stackSetHandler;
    }

    /**
     * Handles stack set deployment.
     *
     * @param input Resource deployment request.
     * @param physicalResourceId Physical ID of existing resource (if present).
     * @return Data about published version.
     */
    public CustomResourceResponse<StackInstance> deployStackInstance(
        StackInstanceRequest input,
        String physicalResourceId
    ) {
        String operationId;
        try {
            operationId = this.updateStackInstance(input);
        } catch (StackInstanceNotFoundException error) {
            operationId = this.createStackInstance(input);
        }

        this.stackSetHandler.waitForStackSetOperation(input.getStackSetName(), operationId);

        var stackInstance = this.cloudFormation.describeStackInstance(
            new DescribeStackInstanceRequest()
                .withStackSetName(input.getStackSetName())
                .withStackInstanceAccount(input.getAccountId())
                .withStackInstanceRegion(input.getRegion())
        )
            .getStackInstance();

        return new CustomResourceResponse<>(stackInstance, StackSetInstanceManager.buildPhysicalResourceId(input));
    }

    /**
     * Handles stack instance deletion.
     *
     * @param input Resource delete request.
     * @param physicalResourceId Physical ID of existing resource (if present).
     * @return Empty response.
     */
    public CustomResourceResponse<StackInstance> deleteStackInstance(
        StackInstanceRequest input,
        String physicalResourceId
    ) {
        var spec = StackSetInstanceManager.parsePhysicalResourceId(physicalResourceId);

        this.cloudFormation.deleteStackInstances(
            new DeleteStackInstancesRequest()
                .withStackSetName(spec.getStackSetName())
                .withAccounts(spec.getAccountId())
                .withRegions(spec.getRegion())
        );

        this.logger.info("Stack instance deleted.");

        return new CustomResourceResponse<>(null, physicalResourceId);
    }

    /**
     * Creates new stack instance.
     *
     * @param input Stack instance specification.
     * @return Stack set operation ID.
     */
    private String createStackInstance(StackInstanceRequest input) {
        this.logger.info(
            "Stack set {} instance not found for account {} in {}, creating new one.",
            input.getStackSetName(),
            input.getAccountId(),
            input.getRegion()
        );

        return this.cloudFormation.createStackInstances(
            new CreateStackInstancesRequest()
                .withStackSetName(input.getStackSetName())
                .withAccounts(input.getAccountId())
                .withRegions(input.getRegion())
                .withParameterOverrides(StackSetInstanceManager.buildSdkParameters(input))
        )
            .getOperationId();
    }

    /**
     * Updates existing stack instance.
     *
     * @param input Stack instance specification.
     * @return Stack set operation ID.
     */
    private String updateStackInstance(StackInstanceRequest input) {
        var stackInstance = this.cloudFormation.describeStackInstance(
            new DescribeStackInstanceRequest()
                .withStackSetName(input.getStackSetName())
                .withStackInstanceAccount(input.getAccountId())
                .withStackInstanceRegion(input.getRegion())
        )
            .getStackInstance();

        this.logger.info(
            "Updating only parameters for stack set {} instance for account {} in {}, creating new one - ID {}.",
            input.getStackSetName(),
            input.getAccountId(),
            input.getRegion(),
            stackInstance.getStackId()
        );

        // we only update parameters
        return this.cloudFormation.updateStackInstances(
            new UpdateStackInstancesRequest()
                .withStackSetName(input.getStackSetName())
                .withAccounts(input.getAccountId())
                .withRegions(input.getRegion())
                .withParameterOverrides(StackSetInstanceManager.buildSdkParameters(input))
        )
            .getOperationId();
    }

    /**
     * Converts key-value mapping into AWS SDK structure.
     *
     * @param input Request data.
     * @return Collection of AWS SDK DTOs.
     */
    private static Collection<Parameter> buildSdkParameters(StackInstanceRequest input) {
        return StackUtils.buildSdkList(
            input.getParameterOverrides(),
            (String key, String value) ->
                new Parameter()
                    .withParameterKey(key)
                    .withParameterValue(value)
        );
    }

    /**
     * Converts string identifier into stack instance specification.
     *
     * @param physicalResourceId Compound identifier.
     * @return Stack instance request specification.
     */
    private static StackInstanceRequest parsePhysicalResourceId(String physicalResourceId) {
        var parts = physicalResourceId.split(":");
        var request = new StackInstanceRequest();
        request.setStackSetName(parts[0]);
        request.setAccountId(parts[1]);
        request.setRegion(parts[2]);
        return request;
    }

    /**
     * Converts stack instance specification into string specification.
     *
     * @param input Stack instance request specification.
     * @return Compound identifier.
     */
    private static String buildPhysicalResourceId(StackInstanceRequest input) {
        return String.format(
            "%s:%s:%s",
            input.getStackSetName(),
            input.getAccountId(),
            input.getRegion()
        );
    }
}