Java tutorial
/** * The MIT License (MIT) * * 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.docker.traceability.core; import com.fasterxml.jackson.databind.ObjectMapper; import hudson.BulkChange; import hudson.Extension; import hudson.XmlFile; import hudson.model.Action; import hudson.model.Api; import hudson.model.Fingerprint; import hudson.model.RootAction; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; import hudson.search.Search; import hudson.search.SearchIndex; import hudson.search.SearchIndexBuilder; import hudson.search.SearchableModelObject; import hudson.security.Permission; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.servlet.ServletException; import jenkins.model.Jenkins; import static jenkins.model.Jenkins.XSTREAM; import org.acegisecurity.AccessDeniedException; import org.apache.commons.lang.StringUtils; import org.jenkinsci.plugins.docker.traceability.api.DockerTraceabilityReport; import org.jenkinsci.plugins.docker.traceability.model.DockerTraceabilityReportListener; import org.jenkinsci.plugins.docker.traceability.DockerTraceabilityPlugin; import org.jenkinsci.plugins.docker.traceability.dockerjava.api.command.InspectContainerResponse; import org.jenkinsci.plugins.docker.traceability.dockerjava.api.command.InspectImageResponse; import org.jenkinsci.plugins.docker.traceability.dockerjava.api.model.Event; import org.jenkinsci.plugins.docker.traceability.dockerjava.api.model.Info; import org.jenkinsci.plugins.docker.traceability.fingerprint.DockerContainerRecord; import org.jenkinsci.plugins.docker.traceability.fingerprint.DockerDeploymentFacet; import org.jenkinsci.plugins.docker.traceability.model.DockerAPIReport; import org.jenkinsci.plugins.docker.traceability.model.DockerEvent; import org.jenkinsci.plugins.docker.traceability.model.DockerEventType; import org.jenkinsci.plugins.docker.traceability.model.DockerInfo; import org.jenkinsci.plugins.docker.traceability.util.FingerprintsHelper; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; 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 org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.interceptor.RequirePOST; /** * Provides a button, which allows to trace fingerprints. * @author Oleg Nenashev */ @Extension @ExportedBean public class DockerTraceabilityRootAction implements RootAction, SearchableModelObject, Saveable { private final static Logger LOGGER = Logger.getLogger(DockerTraceabilityPlugin.class.getName()); private @CheckForNull Set<String> containerIDs; public DockerTraceabilityRootAction() { load(); } public Api getApi() { return new Api(this); } /** * Get a list of all Docker container IDs. * @return Docker container IDs. */ public synchronized @Nonnull Set<String> getContainerIDs() { return (containerIDs == null) ? Collections.<String>emptySet() : new TreeSet<String>(containerIDs); } @Exported public synchronized @Nonnull List<DockerAPIReport> records() { if (containerIDs == null) { return Collections.emptyList(); } final List<DockerAPIReport> res = new ArrayList<DockerAPIReport>(containerIDs.size()); for (String containerId : containerIDs) { DockerAPIReport apiReport = DockerAPIReport.forContainer(containerId); if (apiReport != null) { res.add(apiReport); } } return res; } /** * Adds new container ID to the registry. * If the value already exists, it will be ignored. * @param containerID Container ID. * @throws IOException Cannot save the list to the disk */ public synchronized @Nonnull void addContainerID(@Nonnull String containerID) throws IOException { if (containerIDs == null) { containerIDs = new HashSet<String>(); } if (!containerIDs.contains(containerID)) { containerIDs.add(containerID); save(); } } /** * Removes the container ID from the registry. * @param containerID Container ID. * @throws IOException Cannot save the list to the disk */ public synchronized @Nonnull void removeContainerID(@Nonnull String containerID) throws IOException { if (containerIDs != null && containerIDs.contains(containerID)) { containerIDs.remove(containerID); save(); } } @Restricted(NoExternalUse.class) public @CheckForNull Fingerprint getFingerprint(@Nonnull String containerId) { return DockerTraceabilityHelper.of(containerId); } public @CheckForNull DockerDeploymentFacet getDeploymentFacet(@Nonnull String containerId) { Fingerprint fp = DockerTraceabilityHelper.of(containerId); return (fp != null) ? FingerprintsHelper.getFacet(fp, DockerDeploymentFacet.class) : null; } public @CheckForNull DockerContainerRecord getLastDeploymentRecord(@Nonnull String containerId) { DockerDeploymentFacet facet = getDeploymentFacet(containerId); return (facet != null) ? facet.getLatest() : null; } /** * Gets a last container record for every registered container. * @return List of container records for all entries. */ public synchronized @Nonnull List<DockerContainerRecord> getContainerRecords() { if (containerIDs == null) { return Collections.emptyList(); } final List<DockerContainerRecord> res = new ArrayList<DockerContainerRecord>(containerIDs.size()); for (String containerId : containerIDs) { DockerContainerRecord rec = DockerTraceabilityHelper.getLastContainerRecord(containerId); if (rec != null) { res.add(rec); } } return res; } @Override public String getIconFileName() { final DockerTraceabilityPlugin plugin = DockerTraceabilityPlugin.getInstance(); return plugin.getConfiguration().isShowRootAction() ? "/plugin/docker-traceability/images/24x24/docker.png" : null; } @Override public String getDisplayName() { return "Docker Traceability"; } @Override public String getUrlName() { return "docker-traceability"; } /** * Submits a new event through Jenkins API. * @param inspectData JSON output of docker inspect container (array of container infos) * @param hostName Optional name of the host, which submitted the event * "unknown" by default * @param hostId Optional host ID. * "unknown" by default * @param status Optional status of the container. * By default, an artificial {@link DockerEventType#NONE} will be used. * @param time Optional time when the event happened. * The time is specified in seconds since January 1, 1970, 00:00:00 GMT * Default value - current time * @param environment Optional field, which describes the environment * @param imageName Optional field, which provides the name of the image * @return {@link HttpResponse} * @throws IOException Request processing error * @throws ServletException Servlet error */ //TODO: parameters check @RequirePOST public HttpResponse doSubmitContainerStatus(@QueryParameter(required = true) String inspectData, @QueryParameter(required = false) String hostId, @QueryParameter(required = false) String hostName, @QueryParameter(required = false) String status, @QueryParameter(required = false) long time, @QueryParameter(required = false) @CheckForNull String environment, @QueryParameter(required = false) @CheckForNull String imageName) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.SUBMIT); final ObjectMapper mapper = new ObjectMapper(); final InspectContainerResponse[] inspectContainerResponses = mapper.readValue(inspectData, InspectContainerResponse[].class); final long eventTime = time != 0 ? time : System.currentTimeMillis() / 1000; final String effectiveHostName = StringUtils.isNotBlank(hostName) ? hostName : "unknown"; final String effectiveHostId = StringUtils.isNotBlank(hostId) ? hostId : "unknown"; final String effectiveStatus = StringUtils.isNotBlank(status) ? status.toUpperCase(Locale.ENGLISH) : DockerEventType.NONE.toString(); final String effectiveImageName = hudson.Util.fixEmpty(imageName); final String effectiveEnvironment = hudson.Util.fixEmpty(environment); for (InspectContainerResponse inspectContainerResponse : inspectContainerResponses) { final Event event = new DockerEvent(effectiveStatus, inspectContainerResponse.getImageId(), effectiveHostId, eventTime).toDockerEvent(); final Info hostInfo = new DockerInfo(effectiveHostId, effectiveHostName).toInfo(); DockerTraceabilityReport res = new DockerTraceabilityReport(event, hostInfo, inspectContainerResponse, inspectContainerResponse.getImageId(), effectiveImageName, /* InspectImageResponse */ null, new LinkedList<String>(), effectiveEnvironment); DockerTraceabilityReportListener.fire(res); } return HttpResponses.ok(); } /** * Submits a new {@link DockerTraceabilityReport} via API. * @param json String representation of {@link DockerTraceabilityReport} * @return {@link HttpResponse} * @throws ServletException Servlet error * @throws IOException Processing error */ @RequirePOST public HttpResponse doSubmitReport(@QueryParameter(required = true) String json) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.SUBMIT); ObjectMapper mapper = new ObjectMapper(); final DockerTraceabilityReport report = mapper.readValue(json, DockerTraceabilityReport.class); DockerTraceabilityReportListener.fire(report); return HttpResponses.ok(); } /** * Gets a container {@link Fingerprint} page. * @param req Stapler request * @param rsp Stapler response * @param id Container ID. Method supports full 64-char IDs only. * @throws IOException Request processing error * @throws ServletException Servlet error */ public void doContainer(StaplerRequest req, StaplerResponse rsp, @QueryParameter(required = true) String id) throws IOException, ServletException { checkPermission(Jenkins.READ); Jenkins j = Jenkins.getInstance(); if (j == null) { rsp.sendError(500, "Jenkins is not ready"); return; } String fingerPrintHash = DockerTraceabilityHelper.getContainerHash(id); rsp.sendRedirect2(j.getRootUrl() + "fingerprint/" + fingerPrintHash); } /** * Removes the container reference from the registry. * @param id Container ID. Method supports full 64-char IDs only. * @throws IOException Cannot save the updated {@link DockerTraceabilityRootAction} * @throws ServletException Servlet exception */ @RequirePOST public HttpResponse doDeleteContainer(@QueryParameter(required = true) String id) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.DELETE); removeContainerID(id); return HttpResponses.ok(); } /** * Gets an image {@link Fingerprint} page. * @param req Stapler request * @param rsp Stapler response * @param id Image ID. Method supports full 64-char IDs only. * @throws IOException Request processing error * @throws ServletException Servlet error */ public void doImage(StaplerRequest req, StaplerResponse rsp, @QueryParameter(required = true) String id) throws IOException, ServletException { checkPermission(Jenkins.READ); Jenkins j = Jenkins.getInstance(); if (j == null) { rsp.sendError(500, "Jenkins is not ready"); return; } String fingerPrintHash = DockerTraceabilityHelper.getImageHash(id); rsp.sendRedirect2(j.getRootUrl() + "fingerprint/" + fingerPrintHash); } //TODO: filtering by container ID, imageID, containerName, imageName, hostName, hostID, environment /** * Retrieves the latest container status via API. * The output will be retrieved in JSON. Supports filers. Missing * "since" and "until" * @param id ID of the container, for which the info should be retrieved. * Short container IDs are not supported. * @throws IOException Processing error * @throws ServletException Servlet error * @return Raw JSON output compatible with docker inspect */ public HttpResponse doRawContainerInfo(@QueryParameter(required = true) String id) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.READ_DETAILS); //TODO: check containerID format final DockerTraceabilityReport report = DockerTraceabilityHelper.getLastReport(id); if (report == null) { return HttpResponses.error(404, "No info available for the containerId=" + id); } final InspectContainerResponse inspectInfo = report.getContainer(); if (inspectInfo == null) { assert false : "Input logic should reject such cases"; return HttpResponses.error(500, "Cannot retrieve the container's status"); } // Return raw JSON in the response InspectContainerResponse[] out = { inspectInfo }; return toJSONResponse(out); } //TODO: More filtering /** * Queries container statuses via API. * The output will be retrieved in JSON. Supports filters. * @param id ID of the container, for which the info should be retrieved. * Short container IDs are not supported. * @param mode {@link QueryMode}. Default value - {@link QueryMode#inspectContainer} * @param since Optional starting time. * If the value equals to 0, the filter will be ignored (default in {@link QueryParameter}). * @param until End time. * If the value equals to 0, the filter will be ignored (default in {@link QueryParameter}). * @throws IOException Processing error * @throws ServletException Servlet error * @return Response containing the output JSON. may be an error if something breaks. */ public HttpResponse doQueryContainer(@QueryParameter(required = true) String id, @QueryParameter(required = false) String mode, @QueryParameter(required = false) long since, @QueryParameter(required = false) long until) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.READ_DETAILS); final QueryMode queryMode = QueryMode.fromString(mode); final long maxTime = (until != 0) ? until : Long.MAX_VALUE; final long minTime = (since != 0) ? since : Long.MIN_VALUE; DockerDeploymentFacet facet = DockerDeploymentFacet.getDeploymentFacet(id); if (facet == null) { return HttpResponses.error(404, "No info available for the containerId=" + id); } final SortedSet<DockerContainerRecord> deploymentRecords = facet.getDeploymentRecords(); List<Object> result = new ArrayList<Object>(deploymentRecords.size()); for (DockerContainerRecord record : deploymentRecords) { // time filters final long eventTime = record.getReport().getEvent().getTime(); if (eventTime < minTime || eventTime > maxTime) { continue; } // Report data final DockerTraceabilityReport report = record.getReport(); switch (queryMode) { case all: result.add(report); break; case events: result.add(report.getEvent()); break; case inspectContainer: InspectContainerResponse containerResponse = report.getContainer(); if (containerResponse != null) { result.add(containerResponse); } break; case inspectImage: InspectImageResponse imageResponse = report.getImage(); if (imageResponse != null) { result.add(imageResponse); } break; case hostInfo: result.add(report.getHostInfo()); break; default: throw new IllegalStateException("Unsupported query mode: " + queryMode); } } // Return raw JSON in the response return toJSONResponse(result); } /** * Retrieves the latest raw status via API. * The output will be retrieved in JSON. * @param id ID of the image, for which the info should be retrieved. * Short container IDs are not supported. * @throws IOException Processing error * @throws ServletException Servlet error * @return {@link HttpResponse} */ public HttpResponse doRawImageInfo(@QueryParameter(required = true) String id) throws IOException, ServletException { checkPermission(DockerTraceabilityPlugin.READ_DETAILS); final InspectImageResponse report = DockerTraceabilityHelper.getLastInspectImageResponse(id); if (report == null) { return HttpResponses.error(404, "No info available for the imageId=" + id); } // Return raw JSON in the response InspectImageResponse[] out = { report }; return toJSONResponse(out); } /** * Check permission. * Also prohibits the access if Jenkins has not been started yet. * @param p Permission to be checked * @throws AccessDeniedException Access denied */ private void checkPermission(Permission p) throws AccessDeniedException { final Jenkins j = Jenkins.getInstance(); if (j == null) { throw new AccessDeniedException( "Cannot retrieve Jenkins instance. " + "Probably, the service is starting or shutting down"); } j.checkPermission(p); } /** * Check permission. * @param p Permission to be checked * @retun false if the user has no permission or if Jenkins is unavailable */ private boolean hasPermission(Permission p) { final Jenkins j = Jenkins.getInstance(); return (j == null) ? false : j.hasPermission(p); } @Restricted(NoExternalUse.class) public Permission getRequiredPermission() { return Jenkins.READ; } @Override public Search getSearch() { return new Search(); } @Override public String getSearchName() { return getDisplayName(); } @Override public String getSearchUrl() { return getUrlName(); } @Override public SearchIndex getSearchIndex() { return makeSearchIndex().make(); } private synchronized SearchIndexBuilder makeSearchIndex() { final SearchIndexBuilder searchIndexBuilder = new SearchIndexBuilder(); Jenkins j = Jenkins.getInstance(); if (j == null || containerIDs == null) { return searchIndexBuilder; // cannot construct URLs } // Add container IDs and hashes for each registered container id. for (String containerID : containerIDs) { String containerHash = DockerTraceabilityHelper.getContainerHash(containerID); searchIndexBuilder.add("container?id=" + containerID, containerID, containerHash); } return searchIndexBuilder; } private @Nonnull XmlFile getConfigFile() throws IOException { final Jenkins j = Jenkins.getInstance(); if (j == null) { throw new IOException("Jenkins instance is not ready, cannot retrieve the root directory"); } return new XmlFile(XSTREAM, new File(j.getRootDir(), DockerTraceabilityRootAction.class.getName() + ".xml")); } @Override public void save() throws IOException { if (BulkChange.contains(this)) return; getConfigFile().write(this); SaveableListener.fireOnChange(this, getConfigFile()); } /** * Loads the other data from disk if it's available. */ private synchronized void load() { if (containerIDs != null) { containerIDs.clear(); } XmlFile config = null; try { config = getConfigFile(); if (config.exists()) config.unmarshal(this); } catch (IOException e) { LOGGER.log(Level.SEVERE, "Failed to load the configuration from config path = " + config, e); } } /** * Serves the JSON response. * @param item Data to be serialized to JSON * @return HTTP response with application/json MIME type */ private static HttpResponse toJSONResponse(final Object item) { return new HttpResponse() { @Override public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { ObjectMapper mapper = new ObjectMapper(); rsp.setContentType("application/json;charset=UTF-8"); mapper.writeValue(rsp.getWriter(), item); } }; } private enum QueryMode { inspectContainer, inspectImage, events, hostInfo, all; private static final QueryMode DEFAULT = inspectContainer; public static QueryMode fromString(@CheckForNull String str) { if (str == null) { return DEFAULT; } try { return valueOf(str); } catch (IllegalArgumentException ex) { return DEFAULT; } } } /** * Gets the {@link DockerTraceabilityRootAction} of Jenkins instance. * @return Instance or null if it is not available */ public static @CheckForNull DockerTraceabilityRootAction getInstance() { Jenkins j = Jenkins.getInstance(); if (j == null) { return null; } @CheckForNull DockerTraceabilityRootAction action = null; for (Action rootAction : j.getActions()) { if (rootAction instanceof DockerTraceabilityRootAction) { action = (DockerTraceabilityRootAction) rootAction; break; } } return action; } }