/** * The MIT License * * Copyright (c) 2015, CloudBees, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package org.jenkinsci.plugins.registry.notification.webhook; import hudson.model.*; import hudson.model.Queue; import hudson.security.ACL; import jenkins.model.Jenkins; import jenkins.model.ParameterizedJobMixIn; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.jenkinsci.plugins.registry.notification.Coordinator; import org.jenkinsci.plugins.registry.notification.DockerHubTrigger; import org.jenkinsci.plugins.registry.notification.TriggerStore; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.interceptor.RequirePOST; import org.kohsuke.stapler.interceptor.RespondSuccess; import javax.annotation.Nonnull; import java.io.IOException; import java.net.URLDecoder; import java.util.*; import java.util.logging.Level; import java.util.logging.Logger; public abstract class JSONWebHook implements UnprotectedRootAction { private static final Logger logger = Logger.getLogger(JSONWebHook.class.getName()); public String getIconFileName() { return null; } public String getDisplayName() { return "DockerHub web hook"; } @RequirePOST @RespondSuccess public void doNotify(@QueryParameter(required = false) String payload, StaplerRequest request, StaplerResponse response) throws IOException { WebHookPayload hookPayload = null; if (payload != null) { try { hookPayload = createPushNotification(JSONObject.fromObject(payload)); } catch (Exception e) { logger.log(Level.SEVERE, "Could not parse the web hook payload!", e); } } else { hookPayload = parse(request); } if (hookPayload != null) { for (PushNotification pushNotification : hookPayload.getPushNotifications()) { try { trigger(response, pushNotification); } catch (Exception e) { logger.log(Level.SEVERE, "Could not trigger a job!", e); } } } } /** * Stapler entry for the multi build result page * @param sha the id of the trigger data. */ @Nonnull public ResultPage getDetails(@Nonnull final String sha) throws IOException, InterruptedException { TriggerStore.TriggerEntry entry = TriggerStore.getInstance().getEntry(sha); if (entry != null) { return new ResultPage(entry); } else { return ResultPage.NO_RESULT; } } protected void trigger(StaplerResponse response, final PushNotification pushNotification) throws IOException { final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { return; } ACL.impersonate(ACL.SYSTEM, new Runnable() { @Override public void run() { // search all jobs for DockerHubTrigger for (ParameterizedJobMixIn.ParameterizedJob p : jenkins.getAllItems(ParameterizedJobMixIn.ParameterizedJob.class)) { DockerHubTrigger trigger = DockerHubTrigger.getTrigger(p); if (trigger == null) { logger.log(Level.FINER, "job {0} doesn't have DockerHubTrigger set", p.getName()); continue; } logger.log(Level.FINER, "Inspecting candidate job {0}", p.getName()); Set<String> allRepoNames = trigger.getAllRepoNames(); String repoName = pushNotification.getRepoName(); if (allRepoNames.contains(repoName)) { schedule((Job) p, pushNotification); } } } }); } private void schedule(@Nonnull final Job job, @Nonnull final PushNotification pushNotification) { if (new JobbMixIn(job).schedule(pushNotification.getCause())) { logger.info(pushNotification.getCauseMessage()); Coordinator coordinator = Coordinator.getInstance(); if (coordinator != null) { coordinator.onTriggered(job, pushNotification); } } } /** * If someone wanders in to the index page, redirect to Jenkins root. * * @param response the response object * @throws IOException if so */ public void doIndex(StaplerRequest request, StaplerResponse response) throws IOException { response.sendRedirect(request.getContextPath() + "/"); } protected abstract WebHookPayload createPushNotification(JSONObject data); private WebHookPayload parse(StaplerRequest req) throws IOException { //TODO Actually test what duckerhub is really sending String body = IOUtils.toString(req.getInputStream(), req.getCharacterEncoding()); String contentType = req.getContentType(); if (contentType != null && contentType.startsWith("application/x-www-form-urlencoded")) { body = URLDecoder.decode(body, req.getCharacterEncoding()); } logger.log(Level.FINER, "Received commit hook notification : {0}", body); try { JSONObject payload = JSONObject.fromObject(body); return createPushNotification(payload); } catch (Exception e) { logger.log(Level.SEVERE, "Could not parse the web hook payload!", e); return null; } } /** * Workaround until {@link ParameterizedJobMixIn#getDefaultParametersValues()} gets public. */ static class JobbMixIn<JobT extends Job<JobT, RunT> & ParameterizedJobMixIn.ParameterizedJob & Queue.Task, RunT extends Run<JobT, RunT> & Queue.Executable> extends ParameterizedJobMixIn<JobT, RunT> { /** * Some breathing room to iterate through most/all of the jobs before the first triggered build starts. */ public static final int MIN_QUIET = 3; private JobT the; JobbMixIn(JobT the) { this.the = the; } @Override protected JobT asJob() { return the; } boolean schedule(Cause cause) { if (!asJob().isBuildable()) { return false; } List<Action> queueActions = new LinkedList<Action>(); queueActions.add(new ParametersAction(getParameterValues())); queueActions.add(new CauseAction(cause)); int quiet = Math.max(MIN_QUIET, asJob().getQuietPeriod()); final Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { logger.log(Level.WARNING, "Tried to schedule a build while Jenkins was gone."); return false; } final Queue queue = jenkins.getQueue(); if (queue == null) { throw new IllegalStateException("The queue is not initialized?!"); } Queue.Item i = queue.schedule2(asJob(), quiet, queueActions).getItem(); return i != null && i.getFuture() != null; } private List<ParameterValue> getParameterValues() { Set<ParameterValue> result = new HashSet<ParameterValue>(); if (isParameterized()) { result.addAll(getDefaultParametersValues()); } return Collections.unmodifiableList(new LinkedList<ParameterValue>(result)); } /** * Direct copy from {@link ParameterizedJobMixIn#getDefaultParametersValues()} (version 1.580). * * @return the configured parameters with their default values. */ private List<ParameterValue> getDefaultParametersValues() { ParametersDefinitionProperty paramDefProp = asJob().getProperty(ParametersDefinitionProperty.class); ArrayList<ParameterValue> defValues = new ArrayList<ParameterValue>(); /* * This check is made ONLY if someone will call this method even if isParametrized() is false. */ if (paramDefProp == null) return defValues; /* Scan for all parameter with an associated default values */ for (ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) { ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); if (defaultValue != null) defValues.add(defaultValue); } return defValues; } } }