jetbrains.buildServer.runner.codedeploy.AWSClient.java Source code

Java tutorial

Introduction

Here is the source code for jetbrains.buildServer.runner.codedeploy.AWSClient.java

Source

/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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 jetbrains.buildServer.runner.codedeploy;

import com.amazonaws.services.codedeploy.AmazonCodeDeployClient;
import com.amazonaws.services.codedeploy.model.*;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.transfer.TransferManager;
import com.amazonaws.services.s3.transfer.Upload;
import com.amazonaws.services.s3.transfer.model.UploadResult;
import jetbrains.buildServer.util.CollectionsUtil;
import jetbrains.buildServer.util.Converter;
import jetbrains.buildServer.util.StringUtil;
import jetbrains.buildServer.util.amazon.AWSException;
import jetbrains.buildServer.util.amazon.S3Util;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;

/**
 * @author vbedrosova
 */
public class AWSClient {

    @NotNull
    private final AmazonS3Client myS3Client;
    @NotNull
    private final AmazonCodeDeployClient myCodeDeployClient;
    @Nullable
    private String myDescription;
    @NotNull
    private Listener myListener = new Listener();

    public AWSClient(@NotNull AmazonS3Client s3Client, @NotNull AmazonCodeDeployClient codeDeployClient) {
        myS3Client = s3Client;
        myCodeDeployClient = codeDeployClient;
    }

    @NotNull
    public AWSClient withDescription(@NotNull String description) {
        myDescription = description;
        return this;
    }

    @NotNull
    public AWSClient withListener(@NotNull Listener listener) {
        myListener = listener;
        return this;
    }

    /**
     * Uploads application revision archive to S3 bucket named s3BucketName with the provided key and bundle type.
     * <p>
     * For performing this operation target AWSClient must have corresponding S3 permissions.
     *
     * @param revision     valid application revision containing appspec.yml
     * @param s3BucketName valid S3 bucket name
     * @param s3ObjectKey  valid S3 object key
     */
    public void uploadRevision(@NotNull File revision, @NotNull String s3BucketName, @NotNull String s3ObjectKey) {
        try {
            doUploadRevision(revision, s3BucketName, s3ObjectKey);
        } catch (Throwable t) {
            processFailure(t);
        }
    }

    /**
     * Registers application revision from the specified location for the specified CodeDeploy application.
     * <p>
     * For performing this operation target AWSClient must have corresponding CodeDeploy permissions.
     *
     * @param s3BucketName    valid S3 bucket name
     * @param s3ObjectKey     valid S3 object key
     * @param bundleType      one of zip, tar or tar.gz
     * @param s3ObjectVersion S3 object version (for versioned buckets) or null to use the latest version
     * @param s3ObjectETag    S3 object ETag (file checksum) for object validation or null if no validation should be performed
     * @param applicationName CodeDeploy application name
     */
    public void registerRevision(@NotNull String s3BucketName, @NotNull String s3ObjectKey,
            @NotNull String bundleType, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag,
            @NotNull String applicationName) {
        try {
            doRegisterRevision(
                    getRevisionLocation(s3BucketName, s3ObjectKey, bundleType, s3ObjectVersion, s3ObjectETag),
                    applicationName);
        } catch (Throwable t) {
            processFailure(t);
        }
    }

    /**
     * Creates deployment of the application revision from the specified location for specified application (must be pre-configured) to
     * deploymentGroupName (must be pre-configured) EC2 instances group with
     * deploymentConfigName or default configuration name and waits for deployment finish.
     * <p>
     * For performing this operation target AWSClient must have corresponding CodeDeploy permissions.
     *
     * @param s3BucketName         valid S3 bucket name
     * @param s3ObjectKey          valid S3 object key
     * @param bundleType           one of zip, tar or tar.gz
     * @param s3ObjectVersion      S3 object version (for versioned buckets) or null to use the latest version
     * @param s3ObjectETag         S3 object ETag (file checksum) for object validation or null if no validation should be performed
     * @param applicationName      CodeDeploy application name
     * @param deploymentGroupName  deployment group name
     * @param deploymentConfigName deployment configuration name or null for default deployment configuration
     * @param waitTimeoutSec       seconds to wait for the created deployment finish or fail
     * @param waitIntervalSec      seconds between polling CodeDeploy for the created deployment status
     */
    public void deployRevisionAndWait(@NotNull String s3BucketName, @NotNull String s3ObjectKey,
            @NotNull String bundleType, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag,
            @NotNull String applicationName, @NotNull String deploymentGroupName,
            @NotNull Map<String, String> ec2Tags, @NotNull Collection<String> autoScalingGroups,
            @Nullable String deploymentConfigName, int waitTimeoutSec, int waitIntervalSec,
            boolean rollbackOnFailure, boolean rollbackOnAlarmThreshold) {
        doDeployAndWait(s3BucketName, s3ObjectKey, bundleType, s3ObjectVersion, s3ObjectETag, applicationName,
                deploymentGroupName, ec2Tags, autoScalingGroups, deploymentConfigName, true, waitTimeoutSec,
                waitIntervalSec, rollbackOnFailure, rollbackOnAlarmThreshold);
    }

    /**
     * The same as {@link #deployRevisionAndWait} but without waiting
     */
    public void deployRevision(@NotNull String s3BucketName, @NotNull String s3ObjectKey,
            @NotNull String bundleType, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag,
            @NotNull String applicationName, @NotNull String deploymentGroupName,
            @NotNull Map<String, String> ec2Tags, @NotNull Collection<String> autoScalingGroups,
            @Nullable String deploymentConfigName) {
        doDeployAndWait(s3BucketName, s3ObjectKey, bundleType, s3ObjectVersion, s3ObjectETag, applicationName,
                deploymentGroupName, ec2Tags, autoScalingGroups, deploymentConfigName, false, null, null, false,
                false);
    }

    @SuppressWarnings("ConstantConditions")
    private void doDeployAndWait(@NotNull String s3BucketName, @NotNull String s3ObjectKey,
            @NotNull String bundleType, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag,
            @NotNull String applicationName, @NotNull String deploymentGroupName,
            @NotNull Map<String, String> ec2Tags, @NotNull Collection<String> autoScalingGroups,
            @Nullable String deploymentConfigName, boolean wait, @Nullable Integer waitTimeoutSec,
            @Nullable Integer waitIntervalSec, boolean rollbackOnFailure, boolean rollbackOnAlarmThreshold) {
        try {
            final String deploymentId = createDeployment(
                    getRevisionLocation(s3BucketName, s3ObjectKey, bundleType, s3ObjectVersion, s3ObjectETag),
                    applicationName, deploymentGroupName, ec2Tags, autoScalingGroups, deploymentConfigName,
                    rollbackOnFailure, rollbackOnAlarmThreshold);

            if (wait) {
                waitForDeployment(deploymentId, waitTimeoutSec, waitIntervalSec);
            }
        } catch (Throwable t) {
            processFailure(t);
        }
    }

    private void waitForDeployment(@NotNull String deploymentId, int waitTimeoutSec, int waitIntervalSec) {
        myListener.deploymentWaitStarted(deploymentId);

        final GetDeploymentRequest dRequest = new GetDeploymentRequest().withDeploymentId(deploymentId);

        DeploymentInfo dInfo = myCodeDeployClient.getDeployment(dRequest).getDeploymentInfo();

        long startTime = (dInfo == null || dInfo.getStartTime() == null) ? System.currentTimeMillis()
                : dInfo.getStartTime().getTime();

        while (dInfo == null || dInfo.getCompleteTime() == null) {
            myListener.deploymentInProgress(deploymentId, getInstancesStatus(dInfo));

            if (System.currentTimeMillis() - startTime > waitTimeoutSec * 1000) {
                myListener.deploymentFailed(deploymentId, waitTimeoutSec, getErrorInfo(dInfo),
                        getInstancesStatus(dInfo));
                return;
            }

            try {
                Thread.sleep(waitIntervalSec * 1000);
            } catch (InterruptedException e) {
                processFailure(e);
                return;
            }

            dInfo = myCodeDeployClient.getDeployment(dRequest).getDeploymentInfo();
        }

        if (isSuccess(dInfo)) {
            myListener.deploymentSucceeded(deploymentId, getInstancesStatus(dInfo));
        } else {
            myListener.deploymentFailed(deploymentId, null, getErrorInfo(dInfo), getInstancesStatus(dInfo));
        }
    }

    private void doUploadRevision(@NotNull final File revision, @NotNull final String s3BucketName,
            @NotNull final String s3ObjectKey) throws Throwable {
        myListener.uploadRevisionStarted(revision, s3BucketName, s3ObjectKey);

        final UploadResult uploadResult = doUploadWithTransferManager(revision, s3BucketName, s3ObjectKey);

        myListener.uploadRevisionFinished(revision, s3BucketName, s3ObjectKey, uploadResult.getVersionId(),
                uploadResult.getETag(), myS3Client.getUrl(s3BucketName, s3ObjectKey).toString());
    }

    @NotNull
    private UploadResult doUploadWithTransferManager(@NotNull final File revision,
            @NotNull final String s3BucketName, @NotNull final String s3ObjectKey) throws Throwable {
        return S3Util.withTransferManager(myS3Client, new S3Util.WithTransferManager<Upload>() {
            @NotNull
            @Override
            public Collection<Upload> run(@NotNull TransferManager manager) throws Throwable {
                return Collections.singletonList(manager.upload(s3BucketName, s3ObjectKey, revision));
            }
        }).iterator().next().waitForUploadResult();
    }

    @NotNull
    private RevisionLocation getRevisionLocation(@NotNull String s3BucketName, @NotNull String s3ObjectKey,
            @NotNull String bundleType, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag) {
        final S3Location loc = new S3Location().withBucket(s3BucketName).withKey(s3ObjectKey)
                .withBundleType(bundleType);
        if (StringUtil.isNotEmpty(s3ObjectVersion))
            loc.withVersion(s3ObjectVersion);
        if (StringUtil.isNotEmpty(s3ObjectETag))
            loc.withETag(s3ObjectETag);
        return new RevisionLocation().withRevisionType(RevisionLocationType.S3).withS3Location(loc);
    }

    private void doRegisterRevision(@NotNull RevisionLocation revisionLocation, @NotNull String applicationName) {
        final S3Location s3Location = revisionLocation.getS3Location();
        myListener.registerRevisionStarted(applicationName, s3Location.getBucket(), s3Location.getKey(),
                s3Location.getBundleType(), s3Location.getVersion(), s3Location.getETag());

        myCodeDeployClient.registerApplicationRevision(new RegisterApplicationRevisionRequest()
                .withRevision(revisionLocation).withApplicationName(applicationName)
                .withDescription(getDescription("Application revision registered by ", 100)));

        myListener.registerRevisionFinished(applicationName, s3Location.getBucket(), s3Location.getKey(),
                s3Location.getBundleType(), s3Location.getVersion(), s3Location.getETag());
    }

    @NotNull
    private String createDeployment(@NotNull RevisionLocation revisionLocation, @NotNull String applicationName,
            @NotNull String deploymentGroupName, @NotNull Map<String, String> ec2Tags,
            @NotNull Collection<String> autoScalingGroups, @Nullable String deploymentConfigName,
            boolean rollbackOnFailure, boolean rollbackOnAlarmThreshold) {
        myListener.createDeploymentStarted(applicationName, deploymentGroupName, deploymentConfigName);

        final CreateDeploymentRequest request = new CreateDeploymentRequest().withRevision(revisionLocation)
                .withApplicationName(applicationName).withDeploymentGroupName(deploymentGroupName)
                .withDescription(getDescription("Deployment created by ", 100));

        if (StringUtil.isNotEmpty(deploymentConfigName))
            request.setDeploymentConfigName(deploymentConfigName);
        if (!ec2Tags.isEmpty() || !autoScalingGroups.isEmpty()) {
            request.withTargetInstances(new TargetInstances().withTagFilters(getTagFilters(ec2Tags))
                    .withAutoScalingGroups(autoScalingGroups));
        }
        if (rollbackOnFailure || rollbackOnAlarmThreshold) {
            final AutoRollbackConfiguration rollbackConfiguration = new AutoRollbackConfiguration()
                    .withEnabled(true);
            if (rollbackOnFailure) {
                rollbackConfiguration.withEvents(AutoRollbackEvent.DEPLOYMENT_FAILURE);
            }
            if (rollbackOnAlarmThreshold) {
                rollbackConfiguration.withEvents(AutoRollbackEvent.DEPLOYMENT_STOP_ON_ALARM);
            }
            request.setAutoRollbackConfiguration(rollbackConfiguration);
        }

        final String deploymentId = myCodeDeployClient.createDeployment(request).getDeploymentId();
        myListener.createDeploymentFinished(applicationName, deploymentGroupName, deploymentConfigName,
                deploymentId);
        return deploymentId;
    }

    @NotNull
    private Collection<EC2TagFilter> getTagFilters(@NotNull Map<String, String> ec2Tags) {
        return CollectionsUtil.convertCollection(ec2Tags.entrySet(),
                new Converter<EC2TagFilter, Map.Entry<String, String>>() {
                    @Override
                    public EC2TagFilter createFrom(@NotNull Map.Entry<String, String> e) {
                        return new EC2TagFilter().withKey(e.getKey()).withValue(e.getValue())
                                .withType(EC2TagFilterType.KEY_AND_VALUE);
                    }
                });
    }

    private void processFailure(@NotNull Throwable t) {
        myListener.exception(new AWSException(t));
    }

    private boolean isSuccess(@NotNull DeploymentInfo dInfo) {
        return DeploymentStatus.Succeeded.toString().equals(dInfo.getStatus());
    }

    @NotNull
    private String getDescription(@NotNull String prefix, int threshold) {
        return prefix + CodeDeployUtil.truncateStringValueWithDotsAtCenter(
                StringUtil.isEmptyOrSpaces(myDescription) ? getClass().getName() : myDescription,
                threshold - prefix.length());
    }

    @Contract("null -> null")
    @Nullable
    private Listener.InstancesStatus getInstancesStatus(@Nullable DeploymentInfo dInfo) {
        if (dInfo == null)
            return null;
        if (dInfo.getStatus() == null || dInfo.getDeploymentOverview() == null)
            return null;

        final Listener.InstancesStatus instancesStatus = new Listener.InstancesStatus();
        instancesStatus.status = getHumanReadableStatus(dInfo.getStatus());

        final DeploymentOverview overview = dInfo.getDeploymentOverview();
        instancesStatus.succeeded = getInt(overview.getSucceeded());
        instancesStatus.failed = getInt(overview.getFailed());
        instancesStatus.inProgress = getInt(overview.getInProgress());
        instancesStatus.skipped = getInt(overview.getSkipped());
        instancesStatus.pending = getInt(overview.getPending());

        return instancesStatus;
    }

    private static int getInt(@Nullable Long l) {
        return l == null ? 0 : l.intValue();
    }

    @NotNull
    private String getHumanReadableStatus(@NotNull String status) {
        if (DeploymentStatus.Created.toString().equals(status))
            return "created";
        if (DeploymentStatus.Queued.toString().equals(status))
            return "queued";
        if (DeploymentStatus.InProgress.toString().equals(status))
            return "in progress";
        if (DeploymentStatus.Succeeded.toString().equals(status))
            return "succeeded";
        if (DeploymentStatus.Failed.toString().equals(status))
            return "failed";
        if (DeploymentStatus.Stopped.toString().equals(status))
            return "stopped";
        return CodeDeployConstants.STATUS_IS_UNKNOWN;
    }

    @Contract("null -> null")
    @Nullable
    private Listener.ErrorInfo getErrorInfo(@Nullable DeploymentInfo dInfo) {
        if (dInfo == null)
            return null;

        final ErrorInformation errorInformation = dInfo.getErrorInformation();
        if (errorInformation == null)
            return null;

        final Listener.ErrorInfo errorInfo = new Listener.ErrorInfo();
        errorInfo.message = removeTrailingDot(errorInformation.getMessage());
        errorInfo.code = errorInformation.getCode();
        return errorInfo;
    }

    @Contract("null -> null")
    @Nullable
    private String removeTrailingDot(@Nullable String msg) {
        return (msg != null && msg.endsWith(".")) ? msg.substring(0, msg.length() - 1) : msg;
    }

    public static class Listener {
        void uploadRevisionStarted(@NotNull File revision, @NotNull String s3BucketName,
                @NotNull String s3ObjectKey) {
        }

        void uploadRevisionFinished(@NotNull File revision, @NotNull String s3BucketName,
                @NotNull String s3ObjectKey, @Nullable String s3ObjectVersion, @Nullable String s3ObjectETag,
                @NotNull String url) {
        }

        void registerRevisionStarted(@NotNull String applicationName, @NotNull String s3BucketName,
                @NotNull String s3ObjectKey, @NotNull String bundleType, @Nullable String s3ObjectVersion,
                @Nullable String s3ObjectETag) {
        }

        void registerRevisionFinished(@NotNull String applicationName, @NotNull String s3BucketName,
                @NotNull String s3ObjectKey, @NotNull String bundleType, @Nullable String s3ObjectVersion,
                @Nullable String s3ObjectETag) {
        }

        void createDeploymentStarted(@NotNull String applicationName, @NotNull String deploymentGroupName,
                @Nullable String deploymentConfigName) {
        }

        void createDeploymentFinished(@NotNull String applicationName, @NotNull String deploymentGroupName,
                @Nullable String deploymentConfigName, @NotNull String deploymentId) {
        }

        void deploymentWaitStarted(@NotNull String deploymentId) {
        }

        void deploymentInProgress(@NotNull String deploymentId, @Nullable InstancesStatus instancesStatus) {
        }

        void deploymentFailed(@NotNull String deploymentId, @Nullable Integer timeoutSec,
                @Nullable ErrorInfo errorInfo, @Nullable InstancesStatus instancesStatus) {
        }

        void deploymentSucceeded(@NotNull String deploymentId, @Nullable InstancesStatus instancesStatus) {
        }

        void exception(@NotNull AWSException exception) {
        }

        public static class InstancesStatus {
            int pending;
            int inProgress;
            int succeeded;
            int failed;
            int skipped;
            @Nullable
            String status;
        }

        public static class ErrorInfo {
            @Nullable
            String code;
            @Nullable
            String message;
        }
    }
}