package hudson.plugins.tfs; import com.fasterxml.jackson.databind.ObjectMapper; import hudson.Extension; import hudson.model.BuildAuthorizationToken; import hudson.model.BuildableItem; import hudson.model.Item; import hudson.model.Job; import hudson.model.UnprotectedRootAction; import hudson.plugins.tfs.model.AbstractCommand; import hudson.plugins.tfs.model.BuildCommand; import hudson.plugins.tfs.model.BuildWithParametersCommand; import hudson.plugins.tfs.model.PingCommand; import hudson.plugins.tfs.model.TeamBuildPayload; import hudson.plugins.tfs.util.EndpointHelper; import hudson.plugins.tfs.util.MediaType; import jenkins.model.Jenkins; import jenkins.model.ParameterizedJobMixIn; import jenkins.util.TimeDuration; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringEscapeUtils; import org.kohsuke.stapler.ForwardToView; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_CREATED; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; /** * The endpoint that TFS/Team Services will PUT to when it wants to schedule a build in Jenkins. */ @Extension public class TeamBuildEndpoint implements UnprotectedRootAction { private static final Logger LOGGER = Logger.getLogger(TeamBuildEndpoint.class.getName()); private static final Map<String, AbstractCommand.Factory> COMMAND_FACTORIES_BY_NAME; public static final String URL_NAME = "team-build"; public static final String PARAMETER = "parameter"; public static final String BUILD_SOURCE_BRANCH = "Build.SourceBranch"; public static final String QUEUEJOBTASK_MULTIBRANCH_JOB_BRANCH = "QueueJobTask.MultibranchPipelineBranch"; static final String URL_PREFIX = "/" + URL_NAME + "/"; static { final Map<String, AbstractCommand.Factory> map = new TreeMap<String, AbstractCommand.Factory>(String.CASE_INSENSITIVE_ORDER); map.put("ping", new PingCommand.Factory()); map.put("build", new BuildCommand.Factory()); map.put("buildWithParameters", new BuildWithParametersCommand.Factory()); COMMAND_FACTORIES_BY_NAME = Collections.unmodifiableMap(map); } private String commandName; private String jobName; @Override public String getIconFileName() { return null; } @Override public String getDisplayName() { return null; } @Override public String getUrlName() { return URL_NAME; } public String getCommandName() { return commandName; } public String getJobName() { return jobName; } boolean decodeCommandAndJobNames(final String pathInfo) { if (pathInfo.startsWith(URL_PREFIX)) { final String restOfPath = pathInfo.substring(URL_PREFIX.length()); final int firstSlash = restOfPath.indexOf('/'); if (firstSlash != -1) { commandName = restOfPath.substring(0, firstSlash); if (firstSlash < restOfPath.length() - 1) { final String encodedJobName = restOfPath.substring(firstSlash + 1); try { jobName = URLDecoder.decode(encodedJobName, MediaType.UTF_8.name()); } catch (final UnsupportedEncodingException e) { throw new Error(e); } return true; } } else { commandName = restOfPath; } } return false; } public HttpResponse doIndex(final HttpServletRequest request) throws IOException { final Class<? extends TeamBuildEndpoint> me = this.getClass(); final InputStream stream = me.getResourceAsStream("TeamBuildEndpoint.html"); final Jenkins instance = Jenkins.getInstance(); final String rootUrl = instance.getRootUrl(); final String commandRows = describeCommands(COMMAND_FACTORIES_BY_NAME, URL_NAME); try { final String template = IOUtils.toString(stream, MediaType.UTF_8); final String content = String.format(template, URL_NAME, commandRows, rootUrl); return HttpResponses.html(content); } finally { IOUtils.closeQuietly(stream); } } static String describeCommands(final Map<String, AbstractCommand.Factory> commandMap, final String urlName) { final String newLine = System.getProperty("line.separator"); final StringBuilder sb = new StringBuilder(); for (final Map.Entry<String, AbstractCommand.Factory> commandPair : commandMap.entrySet()) { final String commandName = commandPair.getKey(); final AbstractCommand.Factory factory = commandPair.getValue(); sb.append("<tr>").append(newLine); sb.append("<td valign='top'>").append(commandName).append("</td>").append(newLine); sb.append("<td valign='top'>").append('/').append(urlName).append('/').append(commandName).append('/').append("JOB_NAME").append("</td>").append(newLine); final String rawSample = factory.getSampleRequestPayload(); final String escapedSample = StringEscapeUtils.escapeHtml4(rawSample); sb.append("<td><pre>").append(escapedSample).append("</pre></td>").append(newLine); sb.append("</tr>").append(newLine); } return sb.toString(); } @SuppressWarnings("deprecation" /* We want to do exactly what Jenkins does */) void checkPermission(final Job job, final ParameterizedJobMixIn.ParameterizedJob jobMixin, final StaplerRequest req, final StaplerResponse rsp) throws IOException { final BuildAuthorizationToken authToken = jobMixin.getAuthToken(); hudson.model.BuildAuthorizationToken.checkPermission(job, authToken, req, rsp); } void dispatch(final StaplerRequest req, final StaplerResponse rsp, final TimeDuration delay) throws IOException { try { final JSONObject response = innerDispatch(req, rsp, delay); if (response.containsKey("created")) { rsp.setStatus(SC_CREATED); } else { rsp.setStatus(SC_OK); } rsp.setContentType(MediaType.APPLICATION_JSON_UTF_8); final PrintWriter w = rsp.getWriter(); final String responseJsonString = response.toString(); w.print(responseJsonString); w.println(); } catch (final IllegalArgumentException e) { LOGGER.log(Level.WARNING, "IllegalArgumentException", e); EndpointHelper.error(SC_BAD_REQUEST, e); } catch (final ForwardToView e) { throw e; } catch (final Exception e) { final String template = "Error while performing reaction to '%s' command."; final String message = String.format(template, commandName); LOGGER.log(Level.SEVERE, message, e); EndpointHelper.error(SC_INTERNAL_SERVER_ERROR, e); } } /** * If we are calling this method, it means we didn't find any job or project with jobName. Assuming we are building * multibranch pipeline projects in this case. * * We will try to determine the branch name in the following sequence: * 1. For JenkinsQueueJob task 1.115.0+, we send the branch in "QueueJobTask.MultibranchPipelineBranch". * 2. Check if the jobName is composed from ${multibranch_pipeline}/${branch_name} * 3. Check if the payload has BuildSource variable defined (for PR builds) * * If we can't determine the branch name, throw. */ private String getBranch(final String jobName, final StaplerRequest req) { final String json = req.getParameter("json"); final JSONObject formData = JSONObject.fromObject(json); final TeamBuildPayload payload = EndpointHelper.MAPPER.convertValue(formData, TeamBuildPayload.class); String sourceBranch = payload.BuildVariables.get(QUEUEJOBTASK_MULTIBRANCH_JOB_BRANCH); if (sourceBranch == null || sourceBranch.trim().isEmpty()) { final int idx = jobName.indexOf('/'); if (idx > 0) { sourceBranch = jobName.substring(idx + 1); } else { sourceBranch = payload.BuildVariables.get(BUILD_SOURCE_BRANCH); } } if (sourceBranch == null || sourceBranch.trim().isEmpty()) { throw new IllegalArgumentException("Could not find branch from job name. If building a multibranch" + "pipeline job, the job name should be in the format of '${multibranch pipeline name}/${branch}.'"); } try { return URLEncoder.encode(sourceBranch.replace("refs/heads/", ""), "UTF-8"); } catch (final UnsupportedEncodingException e) { throw new RuntimeException("Failed to encode branch: " + sourceBranch, e); } } private String getJobNameFromNestedFolder(final String jobName) { final int idx = jobName.indexOf('/'); if (idx > 0) { return jobName.substring(0, idx); } return jobName; } private Job getJob(final String jobName, final StaplerRequest req) { final Jenkins jenkins = Jenkins.getInstance(); Job job = jenkins.getItemByFullName(jobName, Job.class); if (job == null) { /* For jobs queued by JenkinsQueueJob task 1.115.0+, the jobname sent over the wire is the real job name * without branch name tagged at the end. * Try get the job as it was specified first, if there is no such job, fall back to existing logic and * assume the jobname is in the format of ${multibranchPipelineJobname}/${branchName]. */ final Item mbPipelineJobItem = jenkins.getItemByFullName(jobName); final Item item = (mbPipelineJobItem != null) ? mbPipelineJobItem : jenkins.getItemByFullName(getJobNameFromNestedFolder(jobName)); if (item != null) { final Collection<? extends Job> allJobs = item.getAllJobs(); final String sourceBranch = getBranch(jobName, req); for (final Job j : allJobs) { if (j.getName().equals(sourceBranch)) { job = j; break; } } } } if (job == null) { throw new IllegalArgumentException("Job: " + jobName + " not found"); } return job; } private JSONObject innerDispatch(final StaplerRequest req, final StaplerResponse rsp, final TimeDuration delay) throws IOException, ServletException { commandName = null; jobName = null; final String pathInfo = req.getPathInfo(); if (!decodeCommandAndJobNames(pathInfo)) { if (commandName == null) { throw new IllegalArgumentException("Command not provided"); } if (jobName == null) { throw new IllegalArgumentException("Job name not provided after command"); } } if (!COMMAND_FACTORIES_BY_NAME.containsKey(commandName)) { throw new IllegalArgumentException("Command not implemented"); } final Job job = getJob(jobName, req); final ParameterizedJobMixIn.ParameterizedJob jobMixin = (ParameterizedJobMixIn.ParameterizedJob) job; checkPermission(job, jobMixin, req, rsp); final TimeDuration actualDelay = delay == null ? new TimeDuration(jobMixin.getQuietPeriod()) : delay; final AbstractCommand.Factory factory = COMMAND_FACTORIES_BY_NAME.get(commandName); final AbstractCommand command = factory.create(); final JSONObject response; final JSONObject formData = req.getSubmittedForm(); final ObjectMapper mapper = EndpointHelper.MAPPER; final TeamBuildPayload teamBuildPayload = mapper.convertValue(formData, TeamBuildPayload.class); final BuildableItem buildable = (BuildableItem) job; response = command.perform(job, buildable, req, formData, mapper, teamBuildPayload, actualDelay); return response; } public void doPing( final StaplerRequest request, final StaplerResponse response, @QueryParameter final TimeDuration delay ) throws IOException { dispatch(request, response, delay); } public void doBuild( final StaplerRequest request, final StaplerResponse response, @QueryParameter final TimeDuration delay ) throws IOException { dispatch(request, response, delay); } public void doBuildWithParameters( final StaplerRequest request, final StaplerResponse response, @QueryParameter final TimeDuration delay ) throws IOException { dispatch(request, response, delay); } }