All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jfrog.hudson.release.UnifiedPromoteBuildAction Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2011 JFrog Ltd.
 * 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 org.jfrog.hudson.release;

import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import hudson.model.AbstractBuild;
import hudson.model.Hudson;
import hudson.model.BuildBadgeAction;
import hudson.model.TaskAction;
import hudson.model.TaskListener;
import hudson.model.TaskThread;
import hudson.model.User;
import hudson.security.ACL;
import hudson.security.Permission;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.StatusLine;
import org.jfrog.build.api.builder.PromotionBuilder;
import org.jfrog.build.client.ArtifactoryBuildInfoClient;
import org.jfrog.hudson.ArtifactoryPlugin;
import org.jfrog.hudson.ArtifactoryServer;
import org.jfrog.hudson.BuildInfoAwareConfigurator;
import org.jfrog.hudson.DeployerOverrider;
import org.jfrog.hudson.PluginSettings;
import org.jfrog.hudson.UserPluginInfo;
import org.jfrog.hudson.util.CredentialResolver;
import org.jfrog.hudson.util.Credentials;
import org.jfrog.hudson.util.ExtractorUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import javax.servlet.ServletException;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;

/**
 * This badge action is added to a successful staged builds. It allows performing additional promotion.
 *
 * @author Noam Y. Tenne
 */
public class UnifiedPromoteBuildAction extends TaskAction
        implements BuildBadgeAction {
    private final AbstractBuild build;
    private final C configurator;

    private String targetStatus;
    private String repositoryKey;
    private String comment;
    private boolean useCopy;
    private boolean includeDependencies;
    private PluginSettings promotionPlugin;

    public UnifiedPromoteBuildAction(AbstractBuild build, C configurator) {
        this.build = build;
        this.configurator = configurator;
    }

    @Override
    protected Permission getPermission() {
        return ArtifactoryPlugin.PROMOTE;
    }

    public String getIconFileName() {
        return "/plugin/artifactory/images/artifactory-release.png";
    }

    public String getDisplayName() {
        return "Artifactory Release Promotion";
    }

    public String getUrlName() {
        if (hasPromotionPermission()) {
            return "promote";
        }
        // return null to hide this action
        return null;
    }

    public boolean hasPromotionPermission() {
        return getACL().hasPermission(getPermission());
    }

    public AbstractBuild getBuild() {
        return build;
    }

    public void setTargetStatus(String targetStatus) {
        this.targetStatus = targetStatus;
    }

    public void setRepositoryKey(String repositoryKey) {
        this.repositoryKey = repositoryKey;
    }

    public void setComment(String comment) {
        this.comment = comment;
    }

    public void setUseCopy(boolean useCopy) {
        this.useCopy = useCopy;
    }

    public void setIncludeDependencies(boolean includeDependencies) {
        this.includeDependencies = includeDependencies;
    }

    public String getPromotionPluginName() {
        return (promotionPlugin != null) ? promotionPlugin.getPluginName() : null;
    }

    public void setPromotionPlugin(PluginSettings promotionPlugin) {
        this.promotionPlugin = promotionPlugin;
    }

    public String getPluginParamValue(String pluginName, String paramKey) {
        return (promotionPlugin != null) ? promotionPlugin.getPluginParamValue(pluginName, paramKey) : null;
    }

    /**
     * @return List of target repositories for deployment (release repositories first). Called from the UI.
     */
    public List getRepositoryKeys() {
        ArtifactoryServer artifactoryServer = configurator.getArtifactoryServer();
        if (artifactoryServer == null) {
            return Lists.newArrayList();
        }
        List repos = artifactoryServer.getReleaseRepositoryKeysFirst();
        repos.add(0, "");  // option not to move
        return repos;
    }

    @SuppressWarnings({"UnusedDeclaration"})
    public List getTargetStatuses() {
        return Lists.newArrayList(/*"Staged", */"Released", "Rolled-back");
    }

    /**
     * @return The repository selected by the latest promotion (to be selected by default).
     */
    public String lastPromotionRepository() {
        // TODO: implement
        return null;
    }

    /**
     * Select which view to display based on the state of the promotion. Will return the form if user selects to perform
     * promotion. Progress will be returned if the promotion is currently in progress.
     */
    @SuppressWarnings({"UnusedDeclaration"})
    public void doIndex(StaplerRequest req, StaplerResponse resp) throws IOException, ServletException {
        req.getView(this, chooseAction()).forward(req, resp);
    }

    /**
     * Form submission is calling this method
     */
    @SuppressWarnings({"UnusedDeclaration"})
    public void doSubmit(StaplerRequest req, StaplerResponse resp) throws IOException, ServletException {
        getACL().checkPermission(getPermission());

        req.bindParameters(this);

        // current user is bound to the thread and will be lost in the perform method
        User user = User.current();
        String ciUser = (user == null) ? "anonymous" : user.getId();

        JSONObject formData = req.getSubmittedForm();
        if (formData.has("promotionPlugin")) {
            JSONObject pluginSettings = formData.getJSONObject("promotionPlugin");
            if (pluginSettings.has("pluginName")) {
                String pluginName = pluginSettings.getString("pluginName");
                if (!UserPluginInfo.NO_PLUGIN_KEY.equals(pluginName)) {
                    PluginSettings settings = new PluginSettings();
                    Map paramMap = Maps.newHashMap();
                    settings.setPluginName(pluginName);
                    Map filteredPluginSettings = Maps.filterKeys(pluginSettings,
                            new Predicate() {
                                public boolean apply(String input) {
                                    return StringUtils.isNotBlank(input) && !"pluginName".equals(input);
                                }
                            });
                    for (Map.Entry settingsEntry : filteredPluginSettings.entrySet()) {
                        String key = settingsEntry.getKey();
                        paramMap.put(key, pluginSettings.getString(key));
                    }
                    paramMap.put("ciUser", ciUser);
                    if (!paramMap.isEmpty()) {
                        settings.setParamMap(paramMap);
                    }
                    setPromotionPlugin(settings);
                }
            }
        }

        ArtifactoryServer server = configurator.getArtifactoryServer();

        new PromoteWorkerThread(server, CredentialResolver.getPreferredDeployer(configurator, server), ciUser).start();

        resp.sendRedirect(".");
    }

    public List getPromotionsUserPluginInfo() {
        ArtifactoryServer artifactoryServer = configurator.getArtifactoryServer();
        if (artifactoryServer == null) {
            return Lists.newArrayList(UserPluginInfo.NO_PLUGIN);
        }
        return artifactoryServer.getPromotionsUserPluginInfo();
    }

    @Override
    protected ACL getACL() {
        return build.getACL();
    }

    private synchronized String chooseAction() {
        return workerThread == null ? "form.jelly" : "progress.jelly";
    }

    /**
     * The thread that performs the promotion asynchronously.
     */
    public final class PromoteWorkerThread extends TaskThread {

        private final ArtifactoryServer artifactoryServer;
        private final Credentials deployer;
        private final String ciUser;

        public PromoteWorkerThread(ArtifactoryServer artifactoryServer, Credentials deployer, String ciUser) {
            super(UnifiedPromoteBuildAction.this, ListenerAndText.forMemory(null));
            this.artifactoryServer = artifactoryServer;
            this.deployer = deployer;
            this.ciUser = ciUser;
        }

        @Override
        protected void perform(TaskListener listener) {
            ArtifactoryBuildInfoClient client = null;
            try {
                long started = System.currentTimeMillis();
                listener.getLogger().println("Promoting build ....");

                client = artifactoryServer.createArtifactoryClient(deployer.getUsername(), deployer.getPassword(),
                        artifactoryServer.createProxyConfiguration(Hudson.getInstance().proxy));

                if ((promotionPlugin != null) &&
                        !UserPluginInfo.NO_PLUGIN_KEY.equals(promotionPlugin.getPluginName())) {
                    handlePluginPromotion(listener, client);
                } else {
                    handleStandardPromotion(listener, client);

                }

                build.save();
                // if the client gets back to the progress (after the redirect) page when this thread already done,
                // she will get an error message because the log dies with the thread. So lets delay up to 3 seconds
                long timeToWait = 2000 - (System.currentTimeMillis() - started);
                if (timeToWait > 0) {
                    Thread.sleep(timeToWait);
                }
                workerThread = null;
            } catch (Throwable e) {
                e.printStackTrace(listener.error(e.getMessage()));
            } finally {
                if (client != null) {
                    client.shutdown();
                }
            }
        }

        private void handlePluginPromotion(TaskListener listener, ArtifactoryBuildInfoClient client)
                throws IOException {
            String buildName = ExtractorUtils.sanitizeBuildName(build.getParent().getFullName());
            String buildNumber = build.getNumber() + "";
            HttpResponse pluginPromotionResponse = client.executePromotionUserPlugin(
                    promotionPlugin.getPluginName(), buildName, buildNumber, promotionPlugin.getParamMap());
            if (checkSuccess(pluginPromotionResponse, false, false, listener)) {
                listener.getLogger().println("Promotion completed successfully!");
            }
        }

        private void handleStandardPromotion(TaskListener listener, ArtifactoryBuildInfoClient client)
                throws IOException {
            // do a dry run first
            PromotionBuilder promotionBuilder = new PromotionBuilder()
                    .status(targetStatus)
                    .comment(comment)
                    .ciUser(ciUser)
                    .targetRepo(repositoryKey)
                    .dependencies(includeDependencies)
                    .copy(useCopy)
                    .dryRun(true);
            listener.getLogger()
                    .println("Performing dry run promotion (no changes are made during dry run) ...");
            String buildName = ExtractorUtils.sanitizeBuildName(build.getParent().getFullName());
            String buildNumber = build.getNumber() + "";
            HttpResponse dryResponse = client.stageBuild(buildName, buildNumber, promotionBuilder.build());
            if (checkSuccess(dryResponse, true, true, listener)) {
                listener.getLogger().println("Dry run finished successfully.\nPerforming promotion ...");
                HttpResponse wetResponse = client.stageBuild(buildName,
                        buildNumber, promotionBuilder.dryRun(false).build());
                if (checkSuccess(wetResponse, false, true, listener)) {
                    listener.getLogger().println("Promotion completed successfully!");
                }
            }
        }

        /**
         * Checks the status and return true on success
         *
         * @param response
         * @param dryRun
         * @param parseMessages
         * @param listener
         * @return
         */
        private boolean checkSuccess(HttpResponse response, boolean dryRun, boolean parseMessages,
                TaskListener listener) {
            StatusLine status = response.getStatusLine();
            try {
                String content = entityToString(response);
                if (assertResponseStatus(dryRun, listener, status, content)) {
                    if (parseMessages) {
                        JSONObject json = JSONObject.fromObject(content);
                        JSONArray messages = json.getJSONArray("messages");
                        for (Object messageObj : messages) {
                            JSONObject messageJson = (JSONObject) messageObj;
                            String level = messageJson.getString("level");
                            String message = messageJson.getString("message");
                            // TODO: we don't want to fail if no items were moved/copied. find a way to support it
                            if ((level.equals("WARNING") || level.equals("ERROR")) &&
                                    !message.startsWith("No items were")) {
                                listener.error("Received " + level + ": " + message);
                                return false;
                            }
                        }
                    }
                    return true;
                }
            } catch (IOException e) {
                e.printStackTrace(listener.error("Failed parsing promotion response:"));
            }
            return false;
        }

        private boolean assertResponseStatus(boolean dryRun, TaskListener listener, StatusLine status, String content) {
            if (status.getStatusCode() != 200) {
                if (dryRun) {
                    listener.error(
                            "Promotion failed during dry run (no change in Artifactory was done): " + status +
                                    "\n" + content);
                } else {
                    listener.error(
                            "Promotion failed. View Artifactory logs for more details: " + status + "\n" + content);
                }
                return false;
            }
            return true;
        }

        private String entityToString(HttpResponse response) throws IOException {
            HttpEntity entity = response.getEntity();
            InputStream is = entity.getContent();
            return IOUtils.toString(is, "UTF-8");
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy