Package org.jenkinsci.plugins.stashNotifier

Source Code of org.jenkinsci.plugins.stashNotifier.StashNotifier$DescriptorImpl

/*
* Copyright 2013 Georg Gruetter
*
* 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.jenkinsci.plugins.stashNotifier;
import hudson.EnvVars;
import hudson.Launcher;
import hudson.Extension;
import hudson.util.FormValidation;
import hudson.ProxyConfiguration;
import hudson.model.AbstractBuild;
import hudson.model.BuildListener;
import hudson.model.AbstractProject;
import hudson.model.Result;
import hudson.plugins.git.util.BuildData;
import hudson.tasks.Publisher;
import hudson.tasks.Notifier;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.util.Secret;
import net.sf.json.JSONObject;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.conn.ssl.TrustStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.util.EntityUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.QueryParameter;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.TrustManager;
import javax.servlet.ServletException;

import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.security.KeyStoreException;
import java.security.UnrecoverableKeyException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.regex.Pattern;

import jenkins.model.Jenkins;

/**
* Notifies a configured Atlassian Stash server instance of build results
* through the Stash build API.
* <p>
* Only basic authentication is supported at the moment.
*/
public class StashNotifier extends Notifier {
 
  // attributes --------------------------------------------------------------

  /** base url of Stash server, e. g. <tt>http://localhost:7990</tt>. */
  private final String stashServerBaseUrl;
 
  /** name of Stash user for authentication with Stash build API. */
  private final String stashUserName;
 
  /** password of Stash user for authentication with Stash build API. */
  private final Secret stashUserPassword;
 
  /** if true, ignore exception thrown in case of an unverified SSL peer. */
  private final boolean ignoreUnverifiedSSLPeer;

  /** specify the commit from config */
  private final String commitSha1;
 
  /** if true, the build number is included in the Stash notification. */
  private final boolean includeBuildNumberInKey;
 
  // public members ----------------------------------------------------------

  public BuildStepMonitor getRequiredMonitorService() {
    return BuildStepMonitor.NONE;
  }

  @DataBoundConstructor
  public StashNotifier(
      String stashServerBaseUrl,
      String stashUserName,
      String stashUserPassword,
      boolean ignoreUnverifiedSSLPeer,
      String commitSha1,
      boolean includeBuildNumberInKey) {
    this.stashServerBaseUrl = stashServerBaseUrl.endsWith("/")
                ? stashServerBaseUrl.substring(0, stashServerBaseUrl.length()-1)
                : stashServerBaseUrl;
    this.stashUserName = stashUserName;
    this.stashUserPassword = Secret.fromString(stashUserPassword);
    this.ignoreUnverifiedSSLPeer
      = ignoreUnverifiedSSLPeer;
    this.commitSha1 = commitSha1;
    this.includeBuildNumberInKey = includeBuildNumberInKey;
  }

  public String getStashServerBaseUrl() {
    return stashServerBaseUrl;
  }

  public String getStashUserName() {
    return stashUserName;
  }

  public String getStashUserPassword() {
    return stashUserPassword.getEncryptedValue();
  }
 
  public boolean getIgnoreUnverifiedSSLPeer() {
    return ignoreUnverifiedSSLPeer;
  }
 
  public String getCommitSha1() {
    return commitSha1;
  }

  public boolean getIncludeBuildNumberInKey() {
    return includeBuildNumberInKey;
  }
 
  @Override
  public boolean prebuild(AbstractBuild<?, ?> build, BuildListener listener) {
    return processJenkinsEvent(build, listener, StashBuildState.INPROGRESS);
  }
 
  @Override
  public boolean perform(
      AbstractBuild<?, ?> build,
      Launcher launcher,
      BuildListener listener) {
   
    if ((build.getResult() == null)
        || (!build.getResult().equals(Result.SUCCESS))) {
      return processJenkinsEvent(
          build, listener, StashBuildState.FAILED);
    } else {
      return processJenkinsEvent(
          build, listener, StashBuildState.SUCCESSFUL);
    }
  }

  /**
   * Processes the Jenkins events triggered before and after the build and
   * initiates the Stash notification.
   *
   * @param build    the build to notify Stash of
   * @param listener  the Jenkins build listener
   * @param state    the state of the build (in progress, success, failed)
   * @return      always true in order not to abort the Job in case of
   *           notification failures
   */
  private boolean processJenkinsEvent(
      final AbstractBuild<?, ?> build,
      final BuildListener listener,
      final StashBuildState state) {
   
    PrintStream logger = listener.getLogger();

    // exit if Jenkins root URL is not configured. Stash build API
    // requires valid link to build in CI system.
    if (Jenkins.getInstance().getRootUrl() == null) {
      logger.println(
          "Cannot notify Stash! (Jenkins Root URL not configured)");
      return true;
    }

    Collection<String> commitSha1s = lookupCommitSha1s(build, listener);
    for  (String commitSha1 : commitSha1s) {
      try {
        NotificationResult result
          = notifyStash(logger, build, commitSha1, listener, state);
        if (result.indicatesSuccess) {
          logger.println(
            "Notified Stash for commit with id "
                + commitSha1);
        } else {
          logger.println(
          "Failed to notify Stash for commit "
              + commitSha1
              + " (" + result.message + ")");
        }         
            } catch (SSLPeerUnverifiedException e) {
          logger.println("SSLPeerUnverifiedException caught while "
            + "notifying Stash. Make sure your SSL certificate on "
            + "your Stash server is valid or check the "
            + " 'Ignore unverifiable SSL certificate' checkbox in the "
            + "Stash plugin configuration of this job.");
      } catch (Exception e) {
        logger.println("Caught exception while notifying Stash with id "
          + commitSha1);
        e.printStackTrace(logger);
      }     
    }
    if (commitSha1s.isEmpty()) {
      logger.println("found no commit info");
    }
    return true;
  }

  private Collection<String> lookupCommitSha1s(
      @SuppressWarnings("rawtypes") AbstractBuild build,
      BuildListener listener) {
   
    if (commitSha1 != null && commitSha1.trim().length() > 0) {
      PrintStream logger = listener.getLogger();
      try {
        EnvVars environment = build.getEnvironment(listener);
        return Arrays.asList(environment.expand(commitSha1));
      } catch (IOException e) {
        logger.println("Unable to expand commit SHA value");
        e.printStackTrace(logger);
        return Arrays.asList();
      } catch (InterruptedException e) {
        logger.println("Unable to expand commit SHA value");
        e.printStackTrace(logger);
        return Arrays.asList();
      }
    }

    // Use a set to remove duplicates
    Collection<String> sha1s = new HashSet<String>();
    // MultiSCM may add multiple BuildData actions for each SCM, but we are covered in any case
    for (BuildData buildData : build.getActions(BuildData.class)) {
      // get the sha1 of the commit that was built
      String sha1 = buildData.getLastBuiltRevision().getSha1String();
      // Should never be null, but may be blank
      if (!sha1.isEmpty()) {
        sha1s.add(sha1);
      }
    }
    return sha1s;
  }

  /**
   * Returns the HttpClient through which the REST call is made. Uses an
   * unsafe TrustStrategy in case the user specified a HTTPS URL and
   * set the ignoreUnverifiedSSLPeer flag.
   *
   * @param logger  the logger to log messages to
   * @return      the HttpClient
   */
  private HttpClient getHttpClient(PrintStream logger) {
    HttpClient client = null;
        boolean ignoreUnverifiedSSL = ignoreUnverifiedSSLPeer;
        String url = stashServerBaseUrl;
        DescriptorImpl descriptor = getDescriptor();
        if ("".equals(url) || url == null) {
            url = descriptor.getStashRootUrl();
        }
        if (!ignoreUnverifiedSSL) {
            ignoreUnverifiedSSL = descriptor.isIgnoreUnverifiedSsl();
        }
        if (url.startsWith("https")
                && ignoreUnverifiedSSL) {
      // add unsafe trust manager to avoid thrown
      // SSLPeerUnverifiedException
      try {
        TrustStrategy easyStrategy = new TrustStrategy() {
            public boolean isTrusted(X509Certificate[] chain, String authType)
                    throws CertificateException {
                return true;
            }
        };

        SSLSocketFactory sslSocketFactory
          = new SSLSocketFactory(easyStrategy);
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(
            new Scheme("https", 443, sslSocketFactory));
        ClientConnectionManager connectionManager
          = new SingleClientConnManager(schemeRegistry);
        client = new DefaultHttpClient(connectionManager);
      } catch (NoSuchAlgorithmException nsae) {
        logger.println("Couldn't establish SSL context:");
        nsae.printStackTrace(logger);
      } catch (KeyManagementException kme) {
        logger.println("Couldn't initialize SSL context:");
        kme.printStackTrace(logger);
      } catch (KeyStoreException kse) {
        logger.println("Couldn't initialize SSL context:");
        kse.printStackTrace(logger);
      } catch (UnrecoverableKeyException uke) {
        logger.println("Couldn't initialize SSL context:");
        uke.printStackTrace(logger);
      } finally {
        if (client == null) {
          logger.println("Trying with safe trust manager, instead!");
          client = new DefaultHttpClient();
        }
      }
    } else {
      client = new DefaultHttpClient();
    }
   
    ProxyConfiguration proxy = Jenkins.getInstance().proxy;
    if(proxy != null && !proxy.name.isEmpty() && !proxy.name.startsWith("http") && !isHostOnNoProxyList(proxy)){
      SchemeRegistry schemeRegistry = client.getConnectionManager().getSchemeRegistry();
      schemeRegistry.register(new Scheme("http", proxy.port, new PlainSocketFactory()));
      client.getParams().setParameter(ConnRoutePNames.DEFAULT_PROXY, new HttpHost(proxy.name, proxy.port));
    }
   
    return client;
  }
 
  /**
   * Returns whether or not the stash host is on the noProxy list
   * as defined in the Jenkins proxy settings
   *
   * @param host     the stash URL
   * @param proxy    the ProxyConfiguration
   * @return         whether or not the host is on the noProxy list
   */
  private boolean isHostOnNoProxyList(ProxyConfiguration proxy) {
      String host = getStashServerBaseUrl();
      if ("".equals(host) || host == null) {
          DescriptorImpl descriptor = getDescriptor();
          host = descriptor.getStashRootUrl();
      }
      if (host != null && proxy.noProxyHost != null) {
            for (Pattern p : ProxyConfiguration.getNoProxyHostPatterns(proxy.noProxyHost)) {
                if (p.matcher(host).matches()) {
                    return true;
                }
            }
      }
      return false;
  }

    /**
     * Hudson defines a method {@link Builder#getDescriptor()}, which
     * returns the corresponding {@link Descriptor} object.
     *
     * Since we know that it's actually {@link DescriptorImpl}, override
     * the method and give a better return type, so that we can access
     * {@link DescriptorImpl} methods more easily.
     *
     * This is not necessary, but just a coding style preference.
     */
    @Override
    public DescriptorImpl getDescriptor() {
        // see Descriptor javadoc for more about what a descriptor is.
        return (DescriptorImpl)super.getDescriptor();
    }

    @Extension
  public static final class DescriptorImpl
    extends BuildStepDescriptor<Publisher> {

        /**
         * To persist global configuration information,
         * simply store it in a field and call save().
         *
         * <p>
         * If you don't want fields to be persisted, use <tt>transient</tt>.
         */

        private String stashUser;
        private Secret stashPassword;
        private String stashRootUrl;
        private boolean ignoreUnverifiedSsl;
        private boolean includeBuildNumberInKey;

        public DescriptorImpl() {
            load();
        }

        public String getStashUser() {
          if ((stashUser != null) && (stashUser.trim().equals(""))) {
            return null;
          } else {
              return stashUser;
          }
        }

        public Secret getStashPassword() {
            return stashPassword;
        }

        public String getEncryptedStashPassword() {
            if (stashPassword != null)
                return stashPassword.getEncryptedValue();
            else
                return null;
        }

        public String getStashRootUrl() {
          if ((stashRootUrl == null) || (stashRootUrl.trim().equals(""))) {
            return null;
          } else {
              return stashRootUrl;
          }
        }

        public boolean isIgnoreUnverifiedSsl() {
            return ignoreUnverifiedSsl;
        }

        public boolean isIncludeBuildNumberInKey() {
            return includeBuildNumberInKey;
        }
       
        public FormValidation doCheckStashServerBaseUrl(
          @QueryParameter String value)
        throws IOException, ServletException {

      // calculate effective url from global and local config
      String url = value;
      if ((url != null) && (!url.trim().equals(""))) {
        url = url.trim();
      } else {
        url = stashRootUrl != null ? stashRootUrl.trim() : null;
      }

      if ((url == null) || url.equals("")) {
        return FormValidation.error(
            "Please specify a valid URL here or in the global "
            + "configuration");
      } else {
        try {
          new URL(url);
          return FormValidation.ok();
        } catch (Exception e) {
          return FormValidation.error(
            "Please specify a valid URL here or in the global "
            + "configuration!");
        }
      }
    }

    public FormValidation doCheckStashUserName(@QueryParameter String value)
        throws IOException, ServletException {

      if (value.trim().equals("")
          && ((stashUser == null) || stashUser.trim().equals(""))) {
        return FormValidation.error(
            "Please specify a user name here or in the global "
            + "configuration!");
      } else {
        return FormValidation.ok();
      }
    }

    public FormValidation doCheckStashUserPassword(
          @QueryParameter String value)
        throws IOException, ServletException {

      if (value.trim().equals("")
          && ((stashPassword == null)
            || stashPassword.getPlainText().trim().equals(""))) {
        return FormValidation.warning(
            "You should use a non-empty password here or in the "
            + "global configuration!");
      } else {
        return FormValidation.ok();
      }
    }

    @SuppressWarnings("rawtypes")
    public boolean isApplicable(Class<? extends AbstractProject> aClass) {
      return true;
    }

    public String getDisplayName() {
      return "Notify Stash Instance";
    }

    @Override
    public boolean configure(
        StaplerRequest req,
        JSONObject formData) throws FormException {

            // to persist global configuration information,
            // set that to properties and call save().
            stashUser
              = formData.getString("stashUser");
            stashPassword
              = Secret.fromString(formData.getString("stashPassword"));
            stashRootUrl
              = formData.getString("stashRootUrl");
            ignoreUnverifiedSsl
              = formData.getBoolean("ignoreUnverifiedSsl");
            includeBuildNumberInKey
              = formData.getBoolean("includeBuildNumberInKey");
      save();
      return super.configure(req,formData);
    }
  }
 
  // non-public members ------------------------------------------------------
 
  /**
   * Notifies the configured Stash server by POSTing the build results
   * to the Stash build API.
   *
   * @param logger    the logger to use
   * @param build      the build to notify Stash of
   * @param commitSha1  the SHA1 of the built commit
   * @param client    the HTTP client with which to execute the request
   * @param listener    the build listener for logging
   * @param state      the state of the build as defined by the Stash API.
   */
  private NotificationResult notifyStash(
      final PrintStream logger,
      final AbstractBuild<?, ?> build,
      final String commitSha1,
      final BuildListener listener,
      final StashBuildState state) throws Exception {
    HttpEntity stashBuildNotificationEntity
      = newStashBuildNotificationEntity(build, state);
    HttpPost req = createRequest(stashBuildNotificationEntity, commitSha1);
    HttpClient client = getHttpClient(logger);
    try {
      HttpResponse res = client.execute(req);
      if (res.getStatusLine().getStatusCode() != 204) {
        return NotificationResult.newFailure(
            EntityUtils.toString(res.getEntity()));
      } else {
        return NotificationResult.newSuccess();
      }
    } finally {
      client.getConnectionManager().shutdown();
    }
  }

  /**
   * Returns the HTTP POST request ready to be sent to the Stash build API for
   * the given build and change set.
   *
   * @param stashBuildNotificationEntity  a entity containing the parameters
   *                     for Stash
   * @param commitSha1  the SHA1 of the commit that was built
   * @return        the HTTP POST request to the Stash build API
   */
  private HttpPost createRequest(
      final HttpEntity stashBuildNotificationEntity,
      final String commitSha1) {
   
    String url = stashServerBaseUrl;
        String username = stashUserName;
        String pwd = Secret.toString(stashUserPassword);
        DescriptorImpl descriptor = getDescriptor();

        if ("".equals(url) || url == null)
            url = descriptor.getStashRootUrl();
        if ("".equals(username) || username == null)
            username = descriptor.getStashUser();
        if ("".equals(pwd) || pwd == null)
            pwd = descriptor.getStashPassword().getPlainText();
   
    HttpPost req = new HttpPost(
        url
        + "/rest/build-status/1.0/commits/"
        + commitSha1);
   
    req.addHeader(BasicScheme.authenticate(
        new UsernamePasswordCredentials(
            username,
            pwd),
        "UTF-8",
        false));
   
    req.addHeader("Content-type", "application/json");
    req.setEntity(stashBuildNotificationEntity);
       
    return req;
  }
 
  /**
   * Returns the HTTP POST entity body with the JSON representation of the
   * builds result to be sent to the Stash build API.
   *
   * @param build      the build to notify Stash of
   * @return        HTTP entity body for POST to Stash build API
   */
  private HttpEntity newStashBuildNotificationEntity(
      final AbstractBuild<?, ?> build,
      final StashBuildState state) throws UnsupportedEncodingException {
   
    JSONObject json = new JSONObject();

        json.put("state", state.name());

        json.put("key", getBuildKey(build));

        // This is to replace the odd character Jenkins injects to separate
        // nested jobs, especially when using the Cloudbees Folders plugin.
        // These characters cause Stash to throw up.
        String fullName = StringEscapeUtils.
                escapeJavaScript(build.getFullDisplayName()).
                replaceAll("\\\\u00BB", "\\/");
        json.put("name", fullName);

        json.put("description", getBuildDescription(build, state));
        json.put("url", Jenkins.getInstance()
            .getRootUrl().concat(build.getUrl()));
       
        return new StringEntity(json.toString());
  }

  /**
   * Returns the build key used in the Stash notification. Includes the
   * build number depending on the user setting.
   *
   * @param   build  the build to notify Stash of
   * @return  the build key for the Stash notification
   */
  private String getBuildKey(final AbstractBuild<?, ?> build) {
    StringBuilder key = new StringBuilder();
    key.append(build.getProject().getName());
        if (includeBuildNumberInKey
            || getDescriptor().isIncludeBuildNumberInKey()) {
      key.append('-').append(build.getNumber());
    }
    key.append('-').append(Jenkins.getInstance().getRootUrl());
    return StringEscapeUtils.escapeJavaScript(key.toString());
  }

  /**
   * Returns the description of the build used for the Stash notification.
   * Uses the build description provided by the Jenkins job, if available.
   *
   * @param build    the build to be described
   * @param state    the state of the build
   * @return      the description of the build
   */
  private String getBuildDescription(
      final AbstractBuild<?, ?> build,
      final StashBuildState state) {
   
    if (build.getDescription() != null
        && build.getDescription().trim().length() > 0) {
     
      return build.getDescription();
    } else {
      switch (state) {
      case INPROGRESS:
              return "building on Jenkins @ "
          + Jenkins.getInstance().getRootUrl();
      default:
              return "built by Jenkins @ "
                + Jenkins.getInstance().getRootUrl();
      }
    }
  }
}
TOP

Related Classes of org.jenkinsci.plugins.stashNotifier.StashNotifier$DescriptorImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.