/*
* 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.docker.commons.fingerprint;
import hudson.BulkChange;
import hudson.model.Fingerprint;
import hudson.model.Run;
import jenkins.model.Jenkins;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import jenkins.model.FingerprintFacet;
import org.apache.commons.lang.StringUtils;
/**
* Entry point into fingerprint related functionalities in Docker.
* This class provide basic methods for both images and containers
*/
public class DockerFingerprints {
private static final Logger LOGGER = Logger.getLogger(DockerFingerprints.class.getName());
private DockerFingerprints() {} // no instantiation
/**
* Gets a fingerprint hash for Docker ID (image or container).
* This method calculates image hash without retrieving a fingerprint by
* {@link DockerFingerprints#of(java.lang.String)}, which may be a high-cost call.
*
* @param id Docker ID (image or container).
* Only 64-char full IDs are supported.
* @return 32-char fingerprint hash
* @throws IllegalArgumentException Invalid ID
*/
public static @Nonnull String getFingerprintHash(@Nonnull String id) {
// Remove the "sha256:" prefix, if it exists
if (id.indexOf("sha256:") == 0) {
id = id.substring(7);
}
if (id.length() != 64) {
throw new IllegalArgumentException("Expecting 64-char full image ID, but got " + id);
}
return id.substring(0, 32);
}
/**
* Gets {@link Fingerprint} for a given docker ID.
* @param id Docker ID (image or container). Only 64-char full IDs are supported.
* @return Created fingerprint or null if it is not found
* @throws IOException Fingerprint loading error
*/
public static @CheckForNull Fingerprint of(@Nonnull String id) throws IOException {
final Jenkins jenkins = Jenkins.getInstance(); // should be not null
return jenkins != null ? jenkins.getFingerprintMap().get(getFingerprintHash(id)) : null;
}
private static @CheckForNull Fingerprint ofNoException(@Nonnull String id) {
try {
return of(id);
} catch (IOException ex) { // The error is not a hazard in CheckForNull logic
LOGGER.log(Level.WARNING, "Cannot retrieve a fingerprint for Docker id="+id, ex);
}
return null;
}
/**
* @deprecated Use {@link #forImage(hudson.model.Run, java.lang.String, java.lang.String)}
*/
public static @Nonnull Fingerprint forImage(@CheckForNull Run<?,?> run, @Nonnull String id) throws IOException {
return forImage(run, id, null);
}
/**
* Get or create a {@link Fingerprint} for the image.
* @param run Origin of the fingerprint (if available)
* @param id Image ID. Only 64-char full IDs are supported.
* @param name Optional name of the image. If null, the image name will be
* constructed using the specified ID.
* @return Fingerprint for the specified ID
* @throws IOException Fingerprint load/save error
* @since TODO
*/
public static @Nonnull Fingerprint forImage(@CheckForNull Run<?,?> run,
@Nonnull String id, @CheckForNull String name) throws IOException {
return forDockerInstance(run, id, name, "Docker image ");
}
/**
* @deprecated Use {@link #forContainer(hudson.model.Run, java.lang.String, java.lang.String)}
*/
@Deprecated
public static @Nonnull Fingerprint forContainer(@CheckForNull Run<?,?> run, @Nonnull String id) throws IOException {
return forContainer(run, id, null);
}
/**
* Get or create a {@link Fingerprint} for the container.
* @param run Origin of the fingerprint (if available)
* @param id Container ID. Only 64-char full IDs are supported.
* @param name Optional name of the container. If null, the container name will be
* constructed using the specified ID.
* @return Fingerprint for the specified ID
* @throws IOException Fingerprint load/save error
* @since TODO
*/
public static @Nonnull Fingerprint forContainer(@CheckForNull Run<?,?> run,
@Nonnull String id, @CheckForNull String name) throws IOException {
return forDockerInstance(run, id, name, "Docker container ");
}
private static @Nonnull Fingerprint forDockerInstance(@CheckForNull Run<?,?> run,
@Nonnull String id, @CheckForNull String name, @Nonnull String prefix) throws IOException {
final Jenkins j = Jenkins.getInstance();
if (j == null) {
throw new IOException("Jenkins instance is not ready");
}
final String imageName = prefix + (StringUtils.isNotBlank(name) ? name : id);
return j.getFingerprintMap().getOrCreate(run, imageName, getFingerprintHash(id));
}
/**
* Retrieves a facet from the {@link Fingerprint}.
* The method suppresses {@link IOException} if a fingerprint loading fails.
* @param <TFacet> Facet type to be retrieved
* @param id Docker item ID. Only 64-char full IDs are supported
* @param facetClass Class to be retrieved
* @return First matching facet. Null may be returned if there is no facet
* or if the loading fails
*/
public static @CheckForNull @SuppressWarnings("unchecked")
<TFacet extends FingerprintFacet> TFacet getFacet
(@Nonnull String id, @Nonnull Class<TFacet> facetClass) {
final Fingerprint fp = ofNoException(id);
return (fp != null) ? getFacet(fp, facetClass) : null;
}
/**
* Retrieves a facet from the {@link Fingerprint}.
* The method suppresses {@link IOException} if a fingerprint loading fails.
* @param <TFacet> Facet type to be retrieved
* @param id Docker item ID. Only 64-char full IDs are supported
* @param facetClass Class to be retrieved
* @return First matching facet. Null may be returned if the loading fails
*/
@SuppressWarnings("unchecked")
public static @Nonnull <TFacet extends FingerprintFacet> Collection<TFacet> getFacets
(@Nonnull String id, @Nonnull Class<TFacet> facetClass) {
final Fingerprint fp = ofNoException(id);
return (fp != null) ? getFacets(fp, facetClass) : Collections.<TFacet>emptySet();
}
//TODO: deprecate and use the core's method when it's available
/**
* Retrieves a facet from the {@link Fingerprint}.
* @param <TFacet> Facet type to be retrieved
* @param fingerprint Fingerprint, which stores facets
* @param facetClass Class to be retrieved
* @return First matching facet.
*/
@SuppressWarnings("unchecked")
public static @CheckForNull <TFacet extends FingerprintFacet> TFacet getFacet
(@Nonnull Fingerprint fingerprint, @Nonnull Class<TFacet> facetClass) {
for ( FingerprintFacet facet : fingerprint.getFacets()) {
if (facetClass.isAssignableFrom(facet.getClass())) {
return (TFacet)facet;
}
}
return null;
}
//TODO: deprecate and use the core's method when it's available
/**
* Retrieves facets from the {@link Fingerprint}.
* @param <TFacet> Facet type to be retrieved
* @param fingerprint Fingerprint, which stores facets
* @param facetClass Facet class to be retrieved
* @return All found facets
*/
public static @Nonnull @SuppressWarnings("unchecked")
<TFacet extends FingerprintFacet> Collection<TFacet> getFacets
(@Nonnull Fingerprint fingerprint, @Nonnull Class<TFacet> facetClass) {
final List<TFacet> res = new LinkedList<TFacet>();
for ( FingerprintFacet facet : fingerprint.getFacets()) {
if (facetClass.isAssignableFrom(facet.getClass())) {
res.add((TFacet)facet);
}
}
return res;
}
/**
* Adds a new {@link ContainerRecord} for the specified image, creating necessary intermediate objects as it goes.
*/
public static void addRunFacet(@Nonnull ContainerRecord record, @Nonnull Run<?,?> run) throws IOException {
String imageId = record.getImageId();
Fingerprint f = forImage(run, imageId);
Collection<FingerprintFacet> facets = f.getFacets();
DockerRunFingerprintFacet runFacet = null;
for (FingerprintFacet facet : facets) {
if (facet instanceof DockerRunFingerprintFacet) {
runFacet = (DockerRunFingerprintFacet) facet;
break;
}
}
BulkChange bc = new BulkChange(f);
try {
if (runFacet == null) {
runFacet = new DockerRunFingerprintFacet(f, System.currentTimeMillis(), imageId);
facets.add(runFacet);
}
runFacet.add(record);
runFacet.addFor(run);
DockerFingerprintAction.addToRun(f, imageId, run);
bc.commit();
} finally {
bc.abort();
}
}
/**
* Creates a new {@link DockerAncestorFingerprintFacet} and {@link DockerDescendantFingerprintFacet} and adds a run.
* Or adds to existing facets.
* @param ancestorImageId the ID of the image specified in a {@code FROM} instruction, or null in case of {@code scratch} (i.e., the descendant is a base image)
* @param descendantImageId the ID of the image which was built
* @param run the build in which the image building occurred
*/
public static void addFromFacet(@CheckForNull String ancestorImageId, @Nonnull String descendantImageId, @Nonnull Run<?,?> run) throws IOException {
long timestamp = System.currentTimeMillis();
if (ancestorImageId != null) {
Fingerprint f = forImage(run, ancestorImageId);
Collection<FingerprintFacet> facets = f.getFacets();
DockerDescendantFingerprintFacet descendantFacet = null;
for (FingerprintFacet facet : facets) {
if (facet instanceof DockerDescendantFingerprintFacet) {
descendantFacet = (DockerDescendantFingerprintFacet) facet;
break;
}
}
BulkChange bc = new BulkChange(f);
try {
if (descendantFacet == null) {
descendantFacet = new DockerDescendantFingerprintFacet(f, timestamp, ancestorImageId);
facets.add(descendantFacet);
}
descendantFacet.addDescendantImageId(descendantImageId);
descendantFacet.addFor(run);
DockerFingerprintAction.addToRun(f, ancestorImageId, run);
bc.commit();
} finally {
bc.abort();
}
}
Fingerprint f = forImage(run, descendantImageId);
Collection<FingerprintFacet> facets = f.getFacets();
DockerAncestorFingerprintFacet ancestorFacet = null;
for (FingerprintFacet facet : facets) {
if (facet instanceof DockerAncestorFingerprintFacet) {
ancestorFacet = (DockerAncestorFingerprintFacet) facet;
break;
}
}
BulkChange bc = new BulkChange(f);
try {
if (ancestorFacet == null) {
ancestorFacet = new DockerAncestorFingerprintFacet(f, timestamp, descendantImageId);
facets.add(ancestorFacet);
}
if (ancestorImageId != null) {
ancestorFacet.addAncestorImageId(ancestorImageId);
}
ancestorFacet.addFor(run);
DockerFingerprintAction.addToRun(f, descendantImageId, run);
bc.commit();
} finally {
bc.abort();
}
}
}