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

org.ligoj.app.plugin.build.jenkins.JenkinsPluginResource Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
/*
 * Licensed under MIT (https://github.com/ligoj/ligoj/blob/master/LICENSE)
 */
package org.ligoj.app.plugin.build.jenkins;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.text.Format;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.ligoj.app.api.SubscriptionStatusWithData;
import org.ligoj.app.iam.IamProvider;
import org.ligoj.app.iam.UserOrg;
import org.ligoj.app.model.Project;
import org.ligoj.app.plugin.build.BuildResource;
import org.ligoj.app.plugin.build.BuildServicePlugin;
import org.ligoj.app.resource.NormalizeFormat;
import org.ligoj.app.resource.plugin.AbstractToolPluginResource;
import org.ligoj.app.resource.plugin.CurlProcessor;
import org.ligoj.app.resource.plugin.CurlRequest;
import org.ligoj.app.resource.plugin.HeaderHttpResponseCallback;
import org.ligoj.app.resource.plugin.OnlyRedirectHttpResponseCallback;
import org.ligoj.app.resource.plugin.XmlUtils;
import org.ligoj.bootstrap.core.resource.BusinessException;
import org.ligoj.bootstrap.core.validation.ValidationJsonException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;

/**
 * Jenkins resource.
 */
@Path(JenkinsPluginResource.URL)
@Service
@Produces(MediaType.APPLICATION_JSON)
public class JenkinsPluginResource extends AbstractToolPluginResource implements BuildServicePlugin {

	/**
	 * Public server URL used to fetch the last available version of the product.
	 */
	@Value("${service-build-jenkins-server:http://mirrors.jenkins-ci.org}")
	private String publicServer;

	@Autowired
	protected IamProvider[] iamProvider;

	@Autowired
	protected XmlUtils xml;

	/**
	 * Plug-in key.
	 */
	public static final String URL = BuildResource.SERVICE_URL + "/jenkins";

	/**
	 * Plug-in key.
	 */
	public static final String KEY = URL.replace('/', ':').substring(1);

	/**
	 * Jenkins user name able to connect to instance.
	 */
	public static final String PARAMETER_USER = KEY + ":user";

	/**
	 * Jenkins user api-token able to connect to instance.
	 */
	public static final String PARAMETER_TOKEN = KEY + ":api-token";

	/**
	 * Jenkins job's name.
	 */
	public static final String PARAMETER_JOB = KEY + ":job";

	/**
	 * Jenkins job's name.
	 */
	public static final String PARAMETER_TEMPLATE_JOB = KEY + ":template-job";

	/**
	 * Web site URL
	 */
	public static final String PARAMETER_URL = KEY + ":url";

	/**
	 * Jenkins version callback to extract the header.
	 */
	private static final HeaderHttpResponseCallback VERSION_CALLBACK = new HeaderHttpResponseCallback("x-jenkins");

	@Override
	public void link(final int subscription) throws MalformedURLException, URISyntaxException {
		final Map parameters = subscriptionResource.getParameters(subscription);

		// Validate the node settings
		validateAdminAccess(parameters);

		// Validate the job settings
		validateJob(parameters);
	}

	@Override
	public void create(final int subscription) throws IOException, URISyntaxException {
		final Map parameters = subscriptionResource.getParameters(subscription);
		// Validate the node settings
		validateAdminAccess(parameters);

		// Get Template configuration
		final String templateJob = parameters.get(PARAMETER_TEMPLATE_JOB);
		final String templateConfigXml = getResource(parameters, "job/" + encode(templateJob) + "/config.xml");

		// update template
		final Project project = subscriptionRepository.findOneExpected(subscription).getProject();
		final UserOrg teamLeader = iamProvider[0].getConfiguration().getUserRepository()
				.findById(project.getTeamLeader());
		final String configXml = templateConfigXml
				.replaceFirst("true", "false")
				.replaceAll("gfi-saas", project.getPkey())
				.replaceAll("[email protected]", teamLeader.getMails().get(0))
				.replaceFirst("().*?()", "$1" + project.getName() + "$2")
				.replaceFirst("().*?()", "$1" + project.getDescription() + "$2");

		// create new job
		final String job = parameters.get(PARAMETER_JOB);
		final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
		final CurlRequest curlRequest = new CurlRequest(HttpMethod.POST,
				jenkinsBaseUrl + "/createItem?name=" + encode(job), configXml, "Content-Type:application/xml");
		try (CurlProcessor curl = new JenkinsCurlProcessor(parameters)) {
			if (!curl.process(curlRequest)) {
				throw new BusinessException("Creating the job for the subscription {} failed.", subscription);
			}
		}
	}

	@Override
	public void delete(final int subscription, final boolean deleteRemoteData)
			throws MalformedURLException, URISyntaxException {
		if (deleteRemoteData) {
			final Map parameters = subscriptionResource.getParameters(subscription);
			// Validate the node settings
			validateAdminAccess(parameters);

			// delete the job
			final String job = parameters.get(PARAMETER_JOB);
			final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
			final CurlRequest curlRequest = new CurlRequest(HttpMethod.POST,
					jenkinsBaseUrl + "/job/" + encode(job) + "/doDelete", StringUtils.EMPTY);
			try (CurlProcessor curl = new JenkinsCurlProcessor(parameters, new OnlyRedirectHttpResponseCallback())) {
				if (!curl.process(curlRequest)) {
					throw new BusinessException("Deleting the job for the subscription {} failed.", subscription);
				}
			}
		}
	}

	/**
	 * Validate the administration connectivity.
	 *
	 * @param parameters
	 *            the administration parameters.
	 * @return job name.
	 */
	protected Job validateJob(final Map parameters) throws MalformedURLException, URISyntaxException {
		// Get job's configuration
		final String job = parameters.get(PARAMETER_JOB);
		final String jobXml = getResource(parameters,
				"api/xml?depth=1&tree=jobs[displayName,name,color]&xpath=hudson/job[name='" + encode(job)
						+ "']&wrapper=hudson");
		if (jobXml == null || "".equals(jobXml)) {
			// Invalid couple PKEY and id
			throw new ValidationJsonException(PARAMETER_JOB, "jenkins-job", job);
		}

		// Retrieve description, status and display name
		final Job result = new Job();
		result.setName(getNodeText(jobXml, "displayName"));
		result.setDescription(getNodeText(jobXml, "description"));
		final String statusNode = StringUtils.defaultString(getNodeText(jobXml, "color"), "disabled");
		result.setStatus(toStatus(statusNode));
		result.setBuilding(statusNode.endsWith("_anime"));
		result.setId(job);
		return result;
	}

	private String encode(final String job) throws MalformedURLException, URISyntaxException {
		return new URI("http", job, "").toURL().getPath();
	}

	/**
	 * Return the node text without using document parser.
	 *
	 * @param xmlContent
	 *            XML content.
	 * @param node
	 *            the node name.
	 * @return trimmed node text or null.
	 */
	private String getNodeText(final String xmlContent, final String node) {
		final Matcher matcher = Pattern.compile("<" + node + ">([^<]*)")
				.matcher(ObjectUtils.defaultIfNull(xmlContent, ""));
		if (matcher.find()) {
			return StringUtils.trimToNull(matcher.group(1));
		}
		return null;
	}

	/**
	 * Validate the basic REST connectivity to Jenkins.
	 *
	 * @param parameters
	 *            the server parameters.
	 * @return the detected Jenkins version.
	 */
	protected String validateAdminAccess(final Map parameters) {
		CurlProcessor.validateAndClose(StringUtils.appendIfMissing(parameters.get(PARAMETER_URL), "/") + "login",
				PARAMETER_URL, "jenkins-connection");

		// Check the user can log-in to Jenkins with the preempted
		// authentication processor
		if (getResource(parameters, "api/xml") == null) {
			throw new ValidationJsonException(PARAMETER_USER, "jenkins-login");
		}

		// Check the user has enough rights to get the master configuration and
		// return the version
		final String version = getVersion(parameters);
		if (version == null) {
			throw new ValidationJsonException(PARAMETER_USER, "jenkins-rights");
		}
		return version;
	}

	/**
	 * Return a Jenkins's resource. Return null when the resource is not found.
	 */
	protected String getResource(final Map parameters, final String resource) {
		return getResource(new JenkinsCurlProcessor(parameters), parameters.get(PARAMETER_URL), resource);
	}

	/**
	 * Return a Jenkins's resource. Return null when the resource is not found.
	 */
	protected String getResource(final CurlProcessor processor, final String url, final String resource) {
		// Get the resource using the preempted authentication
		return processor.get(StringUtils.appendIfMissing(url, "/") + resource);
	}

	@Override
	public String getVersion(final Map parameters) {
		// Check the user has enough rights to get the master configuration and
		// get the master configuration and
		return getResource(new JenkinsCurlProcessor(parameters, VERSION_CALLBACK), parameters.get(PARAMETER_URL),
				"api/json?tree=numExecutors");
	}

	/**
	 * Search the Jenkin's template jobs matching to the given criteria. Name, display name and description are
	 * considered.
	 *
	 * @param node
	 *            the node to be tested with given parameters.
	 * @param criteria
	 *            the search criteria.
	 * @return template job names matching the criteria.
	 */
	@GET
	@Path("template/{node}/{criteria}")
	@Consumes(MediaType.APPLICATION_JSON)
	public List findAllTemplateByName(@PathParam("node") final String node,
			@PathParam("criteria") final String criteria)
			throws SAXException, IOException, ParserConfigurationException {
		return findAllByName(node, criteria, "view/Templates/");
	}

	/**
	 * Search the Jenkin's jobs matching to the given criteria. Name, display name and description are considered.
	 *
	 * @param node
	 *            the node to be tested with given parameters.
	 * @param criteria
	 *            the search criteria.
	 * @return job names matching the criteria.
	 */
	@GET
	@Path("{node}/{criteria}")
	@Consumes(MediaType.APPLICATION_JSON)
	public List findAllByName(@PathParam("node") final String node, @PathParam("criteria") final String criteria)
			throws SAXException, IOException, ParserConfigurationException {
		return findAllByName(node, criteria, null);
	}

	/**
	 * Get Jenkins job name by id.
	 *
	 * @param node
	 *            the node to be tested with given parameters.
	 * @param id
	 *            The job name/identifier.
	 * @return job names matching the criteria.
	 */
	@GET
	@Path("{node}/job/{id}")
	@Consumes(MediaType.APPLICATION_JSON)
	public Job findById(@PathParam("node") final String node, @PathParam("id") final String id)
			throws MalformedURLException, URISyntaxException {
		// Prepare the context, an ordered set of jobs
		final Map parameters = pvResource.getNodeParameters(node);
		parameters.put(PARAMETER_JOB, id);
		return validateJob(parameters);
	}

	/**
	 * Search the Jenkin's jobs matching to the given criteria. Name, display name and description are considered.
	 *
	 * @param node
	 *            the node to be tested with given parameters.
	 * @param criteria
	 *            the search criteria.
	 * @param view
	 *            The optional view URL.
	 * @return job names matching the criteria.
	 */
	private List findAllByName(final String node, final String criteria, final String view)
			throws SAXException, IOException, ParserConfigurationException {

		// Prepare the context, an ordered set of jobs
		final Format format = new NormalizeFormat();
		final String formatCriteria = format.format(criteria);
		final Map parameters = pvResource.getNodeParameters(node);

		// Get the jobs and parse them
		final String url = StringUtils.trimToEmpty(view) + "api/xml?tree=jobs[name,displayName,description,color]";
		final String jobsAsXml = StringUtils.defaultString(getResource(parameters, url), "");
		final InputStream jobsAsInput = IOUtils.toInputStream(jobsAsXml, StandardCharsets.UTF_8);
		final Element hudson = (Element) xml.parse(jobsAsInput).getFirstChild();
		final Map result = new TreeMap<>();
		for (final Element jobNode : DomUtils.getChildElementsByTagName(hudson, "job")) {

			// Extract string data from this job
			final String name = StringUtils.trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "name"));
			final String displayName = StringUtils
					.trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "displayName"));
			final String description = StringUtils
					.trimToEmpty(DomUtils.getChildElementValueByTagName(jobNode, "description"));

			// Check the values of this job
			if (format.format(name).contains(formatCriteria) || format.format(displayName).contains(formatCriteria)
					|| format.format(description).contains(formatCriteria)) {

				// Retrieve description and display name
				final Job job = new Job();
				job.setName(StringUtils.trimToNull(displayName));
				job.setDescription(StringUtils.trimToNull(description));
				job.setId(name);
				job.setStatus(toStatus(DomUtils.getChildElementValueByTagName(jobNode, "color")));
				result.put(format.format(ObjectUtils.defaultIfNull(job.getName(), job.getId())), job);
			}
		}
		return new ArrayList<>(result.values());
	}

	/**
	 * Return the color from the raw color of the job.
	 *
	 * @param color
	 *            Raw color node from the job status.
	 * @return The color without 'anime' flag.
	 */
	private String toStatus(final String color) {
		return StringUtils.removeEnd(StringUtils.defaultString(color, "disabled"), "_anime");
	}

	@Override
	public String getKey() {
		return KEY;
	}

	@Override
	public String getLastVersion() {
		// Get the download index from the default repository
		return getLastVersion(publicServer + "/war/");
	}

	/**
	 * Return the last version available for Jenkins for the given repository URL.
	 */
	protected String getLastVersion(final String repo) {
		// Get the download index
		try (CurlProcessor curl = new CurlProcessor()) {
			final String downloadPage = ObjectUtils.defaultIfNull(curl.get(repo), "");

			// Find the last download link
			final Matcher matcher = Pattern.compile("href=\"([\\d.]+)/\"").matcher(downloadPage);
			String lastVersion = null;
			while (matcher.find()) {
				lastVersion = matcher.group(1);
			}

			// Return the last read version
			return lastVersion;
		}
	}

	@Override
	public boolean checkStatus(final Map parameters) {
		// Status is UP <=> Administration access is UP
		validateAdminAccess(parameters);
		return true;
	}

	@Override
	public SubscriptionStatusWithData checkSubscriptionStatus(final Map parameters)
			throws MalformedURLException, URISyntaxException {
		final SubscriptionStatusWithData nodeStatusWithData = new SubscriptionStatusWithData();
		nodeStatusWithData.put("job", validateJob(parameters));
		return nodeStatusWithData;
	}

	/**
	 * Used to launch the job for the subscription.
	 *
	 * @param subscription
	 *            the subscription to use to locate the Jenkins instance.
	 */
	@POST
	@Path("build/{subscription:\\d+}")
	public void build(@PathParam("subscription") final int subscription) {
		final Map parameters = subscriptionResource.getParameters(subscription);

		// Check the instance is available
		validateAdminAccess(parameters);
		if (!build(parameters, "build") && !build(parameters, "buildWithParameters")) {
			throw new BusinessException("Launching the job for the subscription {} failed.", subscription);
		}
	}

	/**
	 * Launch the job with the URL.
	 *
	 * @param parameters
	 *            Parameters used to define the job
	 * @param url
	 *            URL added to the jenkins's URL to launch the job (can be build or buildWithParameters)
	 * @return The result of the processing.
	 */
	protected boolean build(final Map parameters, final String url) {
		final CurlProcessor processor = new JenkinsCurlProcessor(parameters);
		try {
			final String jenkinsBaseUrl = parameters.get(PARAMETER_URL);
			final String jobName = parameters.get(PARAMETER_JOB);
			return processor.process(new CurlRequest("POST", jenkinsBaseUrl + "/job/" + jobName + "/" + url, null));
		} finally {
			processor.close();
		}
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy