package org.jfrog.hudson.pipeline.docker;
import com.google.common.collect.Sets;
import hudson.model.Run;
import hudson.model.TaskListener;
import jenkins.model.Jenkins;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpResponse;
import org.jfrog.build.api.Artifact;
import org.jfrog.build.api.Dependency;
import org.jfrog.build.api.Module;
import org.jfrog.build.api.builder.ArtifactBuilder;
import org.jfrog.build.api.builder.DependencyBuilder;
import org.jfrog.build.api.search.AqlSearchResult;
import org.jfrog.build.client.ArtifactoryVersion;
import org.jfrog.build.extractor.clientConfiguration.client.ArtifactoryBuildInfoClient;
import org.jfrog.build.extractor.clientConfiguration.client.ArtifactoryDependenciesClient;
import org.jfrog.hudson.ArtifactoryServer;
import org.jfrog.hudson.CredentialsConfig;
import org.jfrog.hudson.pipeline.ArtifactoryConfigurator;
import org.jfrog.hudson.pipeline.docker.utils.DockerUtils;
import org.jfrog.hudson.util.CredentialManager;
import java.io.IOException;
import java.io.Serializable;
import java.util.*;
/**
* Created by romang on 8/9/16.
*/
public class DockerImage implements Serializable {
private final String imageId;
private final String imageTag;
private final String manifest;
private final String targetRepo;
private String agentName = "";
private Properties properties = new Properties();
private final ArtifactoryVersion VIRTUAL_REPOS_SUPPORTED_VERSION = new ArtifactoryVersion("4.8.1");
public DockerImage(String imageId, String imageTag, String targetRepo, String manifest, String agentName) {
this.imageId = imageId;
this.imageTag = imageTag;
this.targetRepo = targetRepo;
this.manifest = manifest;
this.agentName = agentName;
}
public String getImageTag() {
return imageTag;
}
public String getImageId() {
return imageId;
}
public String getAgentName() {
return agentName;
}
public void addProperties(Properties properties) {
this.properties.putAll(properties);
}
public Module generateBuildInfoModule(Run build, TaskListener listener, ArtifactoryConfigurator config, String buildName, String buildNumber, String timestamp) throws IOException {
final String buildProperties = String.format("build.name=%s|build.number=%s|build.timestamp=%s", buildName, buildNumber, timestamp);
Properties artifactProperties = new Properties();
artifactProperties.setProperty("build.name", buildName);
artifactProperties.setProperty("build.number", buildNumber);
artifactProperties.setProperty("build.timestamp", timestamp);
ArtifactoryServer server = config.getArtifactoryServer();
CredentialsConfig preferredResolver = server.getDeployerCredentialsConfig();
ArtifactoryDependenciesClient dependenciesClient = server.createArtifactoryDependenciesClient(
preferredResolver.provideUsername(build.getParent()), preferredResolver.providePassword(build.getParent()),
server.createProxyConfiguration(Jenkins.getInstance().proxy), listener);
CredentialsConfig preferredDeployer = CredentialManager.getPreferredDeployer(config, server);
ArtifactoryBuildInfoClient propertyChangeClient = server.createArtifactoryClient(
preferredDeployer.provideUsername(build.getParent()), preferredDeployer.providePassword(build.getParent()),
server.createProxyConfiguration(Jenkins.getInstance().proxy));
Module buildInfoModule = new Module();
buildInfoModule.setId(imageTag.substring(imageTag.indexOf("/") + 1));
boolean includeVirtualReposSupported = propertyChangeClient.getArtifactoryVersion().isAtLeast(VIRTUAL_REPOS_SUPPORTED_VERSION);
DockerLayers layers = createLayers(dependenciesClient, includeVirtualReposSupported);
setDependenciesAndArtifacts(buildInfoModule, layers, buildProperties, artifactProperties,
dependenciesClient, propertyChangeClient, server);
setProperties(buildInfoModule);
return buildInfoModule;
}
private void setProperties(Module buildInfoModule) {
properties.setProperty("docker.image.id", DockerUtils.getShaValue(imageId));
properties.setProperty("docker.captured.image", imageTag);
buildInfoModule.setProperties(properties);
}
private DockerLayers createLayers(ArtifactoryDependenciesClient dependenciesClient, boolean includeVirtualReposSupported) throws IOException {
String queryStr = getAqlQuery(includeVirtualReposSupported);
AqlSearchResult result = dependenciesClient.searchArtifactsByAql(queryStr);
String imagePath = DockerUtils.getImagePath(imageTag);
DockerLayers layers = new DockerLayers();
for (AqlSearchResult.SearchEntry entry : result.getResults()) {
if (!StringUtils.equals(entry.getPath(), imagePath)) {
continue;
}
Set<String> virtual_repos = Sets.newHashSet(entry.getVirtualRepos());
if (!(StringUtils.equals(entry.getRepo(), targetRepo) || virtual_repos.contains(targetRepo))) {
continue;
}
DockerLayer layer = new DockerLayer(entry);
layers.addLayer(layer);
}
return layers;
}
private void setDependenciesAndArtifacts(Module buildInfoModule, DockerLayers layers, String buildProperties, Properties artifactProperties, ArtifactoryDependenciesClient dependenciesClient, ArtifactoryBuildInfoClient propertyChangeClient, ArtifactoryServer server) throws IOException {
DockerLayer historyLayer = layers.getByDigest(imageId);
if (historyLayer == null) {
return;
}
HttpResponse res = dependenciesClient.downloadArtifact(server.getUrl() + "/" + historyLayer.getFullPath());
int dependencyLayerNum = DockerUtils.getNumberOfDependentLayers(IOUtils.toString(res.getEntity().getContent()));
List<Dependency> dependencies = new ArrayList<Dependency>();
List<Artifact> artifacts = new ArrayList<Artifact>();
Iterator<String> it = DockerUtils.getLayersDigests(manifest).iterator();
for (int i = 0; i < dependencyLayerNum; i++) {
String digest = it.next();
DockerLayer layer = layers.getByDigest(digest);
propertyChangeClient.executeUpdateFileProperty(layer.getFullPath(), buildProperties);
Dependency dependency = new DependencyBuilder().id(layer.getFileName()).sha1(layer.getSha1()).properties(artifactProperties).build();
dependencies.add(dependency);
Artifact artifact = new ArtifactBuilder(layer.getFileName()).sha1(layer.getSha1()).properties(artifactProperties).build();
artifacts.add(artifact);
}
buildInfoModule.setDependencies(dependencies);
while (it.hasNext()) {
String digest = it.next();
DockerLayer layer = layers.getByDigest(digest);
if (layer == null) {
continue;
}
propertyChangeClient.executeUpdateFileProperty(layer.getFullPath(), buildProperties);
Artifact artifact = new ArtifactBuilder(layer.getFileName()).sha1(layer.getSha1()).properties(artifactProperties).build();
artifacts.add(artifact);
}
buildInfoModule.setArtifacts(artifacts);
}
/**
* Prepare AQL query to get all the manifest layers from Artifactory.
* Needed for build-info sha1/md5 checksum for each artifact and dependency.
*
* @return
* @throws IOException
*/
private String getAqlQuery(boolean includeVirtualRepos) throws IOException {
List<String> layersDigest = DockerUtils.getLayersDigests(manifest);
StringBuilder aqlRequestForDockerSha = new StringBuilder("items.find({\"$or\":[ ");
List<String> layersQuery = new ArrayList<String>();
for (String digest : layersDigest) {
String shaVersion = DockerUtils.getShaVersion(digest);
String shaValue = DockerUtils.getShaValue(digest);
String singleFileQuery = String.format("{\"name\": \"%s\"}", DockerUtils.digestToFileName(digest));
if (StringUtils.equalsIgnoreCase(shaVersion, "sha1")) {
singleFileQuery = String.format("{\"actual_sha1\": \"%s\"}", shaValue);
}
layersQuery.add(singleFileQuery);
}
aqlRequestForDockerSha.append(StringUtils.join(layersQuery, ","));
if (includeVirtualRepos) {
aqlRequestForDockerSha.append("]}).include(\"name\",\"repo\",\"path\",\"actual_sha1\",\"virtual_repos\")");
} else {
aqlRequestForDockerSha.append("]}).include(\"name\",\"repo\",\"path\",\"actual_sha1\")");
}
return aqlRequestForDockerSha.toString();
}
}