Add cloud native buildpack module
Add a Java implementation of the buildpacks.io specification allowing projects to be packaged into OCI containers. The `builder` class provides a Java equivalent of `pack build` command and is based on the `pack` CLI Go code published at https://github.com/buildpacks/pack. Closes gh-19828pull/19835/head
parent
7fe79f3574
commit
aa1954717c
@ -0,0 +1,28 @@
|
||||
plugins {
|
||||
id 'java-library'
|
||||
id 'org.springframework.boot.conventions'
|
||||
id 'org.springframework.boot.deployed'
|
||||
id 'org.springframework.boot.internal-dependency-management'
|
||||
}
|
||||
|
||||
description = 'Spring Boot Cloud Native Buildpack'
|
||||
|
||||
dependencies {
|
||||
api platform(project(':spring-boot-project:spring-boot-parent'))
|
||||
api 'com.fasterxml.jackson.core:jackson-databind'
|
||||
api 'com.fasterxml.jackson.module:jackson-module-parameter-names'
|
||||
api 'net.java.dev.jna:jna-platform'
|
||||
api 'org.apache.commons:commons-compress:1.19'
|
||||
api 'org.apache.httpcomponents:httpclient'
|
||||
api 'org.springframework:spring-core'
|
||||
|
||||
testImplementation project(':spring-boot-project:spring-boot-tools:spring-boot-test-support')
|
||||
testImplementation 'com.jayway.jsonpath:json-path'
|
||||
testImplementation 'org.assertj:assertj-core'
|
||||
testImplementation 'org.testcontainers:testcontainers'
|
||||
testImplementation 'org.hamcrest:hamcrest'
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
testImplementation 'org.mockito:mockito-core'
|
||||
testImplementation 'org.mockito:mockito-junit-jupiter'
|
||||
testImplementation 'org.skyscreamer:jsonassert'
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
|
||||
/**
|
||||
* Base class for {@link BuildLog} implementations.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public abstract class AbstractBuildLog implements BuildLog {
|
||||
|
||||
@Override
|
||||
public void start(BuildRequest request) {
|
||||
log("Building image '" + request.getName() + "'");
|
||||
log();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference) {
|
||||
return getProgressConsumer(" > Pulling builder image '" + imageReference + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pulledBulder(BuildRequest request, Image image) {
|
||||
log(" > Pulled builder image '" + getDigest(image) + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference) {
|
||||
return getProgressConsumer(" > Pulling run image '" + imageReference + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pulledRunImage(BuildRequest request, Image image) {
|
||||
log(" > Pulled run image '" + getDigest(image) + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume) {
|
||||
log(" > Executing lifecycle version " + version);
|
||||
log(" > Using build cache volume '" + buildCacheVolume + "'");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Consumer<LogUpdateEvent> runningPhase(BuildRequest request, String name) {
|
||||
log();
|
||||
log(" > Running " + name);
|
||||
String prefix = String.format(" %-14s", "[" + name + "] ");
|
||||
return (event) -> log(prefix + event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executedLifecycle(BuildRequest request) {
|
||||
log();
|
||||
log("Successfully built image '" + request.getName() + "'");
|
||||
log();
|
||||
}
|
||||
|
||||
private String getDigest(Image image) {
|
||||
List<String> digests = image.getDigests();
|
||||
return (digests.isEmpty() ? "" : digests.get(0));
|
||||
}
|
||||
|
||||
protected void log() {
|
||||
log("");
|
||||
}
|
||||
|
||||
protected abstract void log(String message);
|
||||
|
||||
protected abstract Consumer<TotalProgressEvent> getProgressConsumer(String message);
|
||||
|
||||
}
|
@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* API Version number comprised a major and minor value.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class ApiVersion {
|
||||
|
||||
/**
|
||||
* The platform API version supported by this release.
|
||||
*/
|
||||
static final ApiVersion PLATFORM = new ApiVersion(0, 1);
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("^v?(\\d+)\\.(\\d*)$");
|
||||
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
|
||||
private ApiVersion(int major, int minor) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the major version number.
|
||||
* @return the major version
|
||||
*/
|
||||
int getMajor() {
|
||||
return this.major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minor version number.
|
||||
* @return the minor version
|
||||
*/
|
||||
int getMinor() {
|
||||
return this.minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that this API version supports the specified version.
|
||||
* @param other the version to check against
|
||||
* @see #supports(ApiVersion)
|
||||
*/
|
||||
void assertSupports(ApiVersion other) {
|
||||
if (!supports(other)) {
|
||||
throw new IllegalStateException(
|
||||
"Version '" + other + "' is not supported by this version ('" + this + "')");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns if this API version supports the given version. A {@code 0.x} matches only
|
||||
* the same version number. A 1.x or higher release matches when the versions have the
|
||||
* same major version and a minor that is equal or greater.
|
||||
* @param other the version to check against
|
||||
* @return of the specified API is supported
|
||||
* @see #assertSupports(ApiVersion)
|
||||
*/
|
||||
boolean supports(ApiVersion other) {
|
||||
if (equals(other)) {
|
||||
return true;
|
||||
}
|
||||
if (this.major == 0 || this.major != other.major) {
|
||||
return false;
|
||||
}
|
||||
return this.minor >= other.minor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ApiVersion other = (ApiVersion) obj;
|
||||
return (this.major == other.major) && (this.minor == other.minor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.major * 31 + this.minor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "v" + this.major + "." + this.minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to parse a string into an {@link ApiVersion} instance.
|
||||
* @param value the value to parse.
|
||||
* @return the corresponding {@link ApiVersion}
|
||||
* @throws IllegalArgumentException if the value could not be parsed
|
||||
*/
|
||||
static ApiVersion parse(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
Matcher matcher = PATTERN.matcher(value);
|
||||
Assert.isTrue(matcher.matches(), "Malformed version number '" + value + "'");
|
||||
try {
|
||||
int major = Integer.parseInt(matcher.group(1));
|
||||
int minor = Integer.parseInt(matcher.group(2));
|
||||
return new ApiVersion(major, minor);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Malformed version number '" + value + "'", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
|
||||
/**
|
||||
* Callback interface used to provide {@link Builder} output logging.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see #toSystemOut()
|
||||
*/
|
||||
public interface BuildLog {
|
||||
|
||||
/**
|
||||
* Log that a build is starting.
|
||||
* @param request the build request
|
||||
*/
|
||||
void start(BuildRequest request);
|
||||
|
||||
/**
|
||||
* Log that the builder image is being pulled.
|
||||
* @param request the build request
|
||||
* @param imageReference the builder image reference
|
||||
* @return a consumer for progress update events
|
||||
*/
|
||||
Consumer<TotalProgressEvent> pullingBuilder(BuildRequest request, ImageReference imageReference);
|
||||
|
||||
/**
|
||||
* Log that the builder image has been pulled.
|
||||
* @param request the build request
|
||||
* @param image the builder image that was pulled
|
||||
*/
|
||||
void pulledBulder(BuildRequest request, Image image);
|
||||
|
||||
/**
|
||||
* Log that a run image is being pulled.
|
||||
* @param request the build request
|
||||
* @param imageReference the run image reference
|
||||
* @return a consumer for progress update events
|
||||
*/
|
||||
Consumer<TotalProgressEvent> pullingRunImage(BuildRequest request, ImageReference imageReference);
|
||||
|
||||
/**
|
||||
* Log that a run image has been pulled.
|
||||
* @param request the build request
|
||||
* @param image the run image that was pulled
|
||||
*/
|
||||
void pulledRunImage(BuildRequest request, Image image);
|
||||
|
||||
/**
|
||||
* Log that the lifecycle is executing.
|
||||
* @param request the build request
|
||||
* @param version the lifecyle version
|
||||
* @param buildCacheVolume the name of the build cache volume in use
|
||||
*/
|
||||
void executingLifecycle(BuildRequest request, LifecycleVersion version, VolumeName buildCacheVolume);
|
||||
|
||||
/**
|
||||
* Log that a specific phase is running.
|
||||
* @param request the build request
|
||||
* @param name the name of the phase
|
||||
* @return a consumer for log updates
|
||||
*/
|
||||
Consumer<LogUpdateEvent> runningPhase(BuildRequest request, String name);
|
||||
|
||||
/**
|
||||
* Log that the lifecycle has executed.
|
||||
* @param request the build request
|
||||
*/
|
||||
void executedLifecycle(BuildRequest request);
|
||||
|
||||
/**
|
||||
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
|
||||
* @return a build log instance that logs to system out
|
||||
*/
|
||||
static BuildLog toSystemOut() {
|
||||
return to(System.out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method that returns a {@link BuildLog} the outputs to a given
|
||||
* {@link PrintStream}.
|
||||
* @param out the print stream used to output the log
|
||||
* @return a build log instance that logs to the given print stream
|
||||
*/
|
||||
static BuildLog to(PrintStream out) {
|
||||
return new PrintStreamBuildLog(out);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* The {@link Owner} that should perform the build.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuildOwner implements Owner {
|
||||
|
||||
private static final String USER_PROPERTY_NAME = "CNB_USER_ID";
|
||||
|
||||
private static final String GROUP_PROPERTY_NAME = "CNB_GROUP_ID";
|
||||
|
||||
private final long uid;
|
||||
|
||||
private final long gid;
|
||||
|
||||
BuildOwner(Map<String, String> env) {
|
||||
this.uid = getValue(env, USER_PROPERTY_NAME);
|
||||
this.gid = getValue(env, GROUP_PROPERTY_NAME);
|
||||
}
|
||||
|
||||
BuildOwner(long uid, long gid) {
|
||||
this.uid = uid;
|
||||
this.gid = gid;
|
||||
}
|
||||
|
||||
private long getValue(Map<String, String> env, String name) {
|
||||
String value = env.get(name);
|
||||
Assert.state(StringUtils.hasText(value), "Missing '" + name + "' value from the builder environment");
|
||||
try {
|
||||
return Long.parseLong(value);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalStateException("Malformed '" + name + "' value '" + value + "' in the builder environment",
|
||||
ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUid() {
|
||||
return this.uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGid() {
|
||||
return this.gid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.uid + "/" + this.gid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create the {@link BuildOwner} by inspecting the image env for
|
||||
* {@code CNB_USER_ID}/{@code CNB_GROUP_ID} variables.
|
||||
* @param env the env to parse
|
||||
* @return a {@link BuildOwner} instance extracted from the env
|
||||
* @throws IllegalStateException if the env does not contain the correct CNB variables
|
||||
*/
|
||||
static BuildOwner fromEnv(Map<String, String> env) {
|
||||
Assert.notNull(env, "Env must not be null");
|
||||
return new BuildOwner(env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link BuildOwner} with specified user/group
|
||||
* identifier.
|
||||
* @param uid the user identifier
|
||||
* @param gid the group identifier
|
||||
* @return a new {@link BuildOwner} instance
|
||||
*/
|
||||
static BuildOwner of(long uid, long gid) {
|
||||
return new BuildOwner(uid, gid);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A build request to be handled by the {@link Builder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class BuildRequest {
|
||||
|
||||
private static final ImageReference DEFAULT_BUILDER = ImageReference.of("cloudfoundry/cnb:0.0.43-bionic");
|
||||
|
||||
private final ImageReference name;
|
||||
|
||||
private final Function<Owner, TarArchive> applicationContent;
|
||||
|
||||
private final ImageReference builder;
|
||||
|
||||
private final Map<String, String> env;
|
||||
|
||||
private final boolean cleanCache;
|
||||
|
||||
private final boolean versboseLogging;
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Assert.notNull(applicationContent, "ApplicationContent must not be null");
|
||||
this.name = name.inTaggedForm();
|
||||
this.applicationContent = applicationContent;
|
||||
this.builder = DEFAULT_BUILDER;
|
||||
this.env = Collections.emptyMap();
|
||||
this.cleanCache = false;
|
||||
this.versboseLogging = false;
|
||||
}
|
||||
|
||||
BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
|
||||
Map<String, String> env, boolean cleanCache, boolean versboseLogging) {
|
||||
this.name = name;
|
||||
this.applicationContent = applicationContent;
|
||||
this.builder = builder;
|
||||
this.env = env;
|
||||
this.cleanCache = cleanCache;
|
||||
this.versboseLogging = versboseLogging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an updated builder.
|
||||
* @param builder the new builder to use
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withBuilder(ImageReference builder) {
|
||||
Assert.notNull(builder, "Builder must not be null");
|
||||
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedForm(), this.env, this.cleanCache,
|
||||
this.versboseLogging);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an additional env variable.
|
||||
* @param name the variable name
|
||||
* @param value the variable value
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withEnv(String name, String value) {
|
||||
Assert.hasText(name, "Name must not be empty");
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
Map<String, String> env = new LinkedHashMap<String, String>(this.env);
|
||||
env.put(name, value);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, Collections.unmodifiableMap(env),
|
||||
this.cleanCache, this.versboseLogging);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an additional env variables.
|
||||
* @param env the additional variables
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withEnv(Map<String, String> env) {
|
||||
Assert.notNull(env, "Env must not be null");
|
||||
Map<String, String> updatedEnv = new LinkedHashMap<String, String>(this.env);
|
||||
updatedEnv.putAll(env);
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder,
|
||||
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.versboseLogging);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an specific clean cache settings.
|
||||
* @param cleanCache if the cache should be cleaned
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withCleanCache(boolean cleanCache) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, cleanCache,
|
||||
this.versboseLogging);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link BuildRequest} with an specific verbose logging settings.
|
||||
* @param verboseLogging if verbose logging should be used
|
||||
* @return an updated build request
|
||||
*/
|
||||
public BuildRequest withVerboseLogging(boolean verboseLogging) {
|
||||
return new BuildRequest(this.name, this.applicationContent, this.builder, this.env, this.cleanCache,
|
||||
verboseLogging);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the image that should be created.
|
||||
* @return the name of the image
|
||||
*/
|
||||
public ImageReference getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@link TarArchive} containing the application content that the buildpack
|
||||
* should package. This is typically the contents of the Jar.
|
||||
* @param owner the owner of the tar entries
|
||||
* @return the application content
|
||||
* @see TarArchive#fromZip(File, Owner)
|
||||
*/
|
||||
public TarArchive getApplicationContent(Owner owner) {
|
||||
return this.applicationContent.apply(owner);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the builder that should be used.
|
||||
* @return the builder to use
|
||||
*/
|
||||
public ImageReference getBuilder() {
|
||||
return this.builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return any env variable that should be passed to the builder.
|
||||
* @return the builder env
|
||||
*/
|
||||
public Map<String, String> getEnv() {
|
||||
return this.env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if caches should be cleaned before packaging.
|
||||
* @return if caches should be cleaned
|
||||
*/
|
||||
public boolean isCleanCache() {
|
||||
return this.cleanCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if verbose logging output should be used.
|
||||
* @return if verbose logging should be used
|
||||
*/
|
||||
public boolean isVerboseLogging() {
|
||||
return this.versboseLogging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link BuildRequest} from a JAR file.
|
||||
* @param jarFile the source jar file
|
||||
* @return a new build request instance
|
||||
*/
|
||||
public static BuildRequest forJarFile(File jarFile) {
|
||||
assertJarFile(jarFile);
|
||||
return forJarFile(ImageReference.forJarFile(jarFile).inTaggedForm(), jarFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link BuildRequest} from a JAR file.
|
||||
* @param name the name of the image that should be created
|
||||
* @param jarFile the source jar file
|
||||
* @return a new build request instance
|
||||
*/
|
||||
public static BuildRequest forJarFile(ImageReference name, File jarFile) {
|
||||
assertJarFile(jarFile);
|
||||
return new BuildRequest(name, (owner) -> TarArchive.fromZip(jarFile, owner));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link BuildRequest} with specific content.
|
||||
* @param name the name of the image that should be created
|
||||
* @param applicationContent function to provide the application content
|
||||
* @return a new build request instance
|
||||
*/
|
||||
public static BuildRequest of(ImageReference name, Function<Owner, TarArchive> applicationContent) {
|
||||
return new BuildRequest(name, applicationContent);
|
||||
}
|
||||
|
||||
private static void assertJarFile(File jarFile) {
|
||||
Assert.notNull(jarFile, "JarFile must not be null");
|
||||
Assert.isTrue(jarFile.exists(), "JarFile must exist");
|
||||
Assert.isTrue(jarFile.isFile(), "JarFile must be a file");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.build.BuilderMetadata.Stack;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerException;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressPullListener;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.UpdateListener;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Central API for running buildpack operations.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Builder {
|
||||
|
||||
private final BuildLog log;
|
||||
|
||||
private final DockerApi docker;
|
||||
|
||||
public Builder() {
|
||||
this(BuildLog.toSystemOut());
|
||||
}
|
||||
|
||||
public Builder(BuildLog log) {
|
||||
this(log, new DockerApi());
|
||||
}
|
||||
|
||||
Builder(BuildLog log, DockerApi docker) {
|
||||
Assert.notNull(log, "Log must not be null");
|
||||
this.log = log;
|
||||
this.docker = docker;
|
||||
}
|
||||
|
||||
public void build(BuildRequest request) throws DockerException, IOException {
|
||||
Assert.notNull(request, "Request must not be null");
|
||||
this.log.start(request);
|
||||
Image builderImage = pullBuilder(request);
|
||||
BuilderMetadata builderMetadata = BuilderMetadata.fromImage(builderImage);
|
||||
BuildOwner buildOwner = BuildOwner.fromEnv(builderImage.getConfig().getEnv());
|
||||
StackId stackId = StackId.fromImage(builderImage);
|
||||
ImageReference runImageReference = getRunImageReference(builderMetadata.getStack());
|
||||
Image runImage = pullRunImage(request, runImageReference);
|
||||
assertHasExpectedStackId(runImage, stackId);
|
||||
EphemeralBuilder builder = new EphemeralBuilder(buildOwner, builderImage, builderMetadata, request.getEnv());
|
||||
this.docker.image().load(builder.getArchive(), UpdateListener.none());
|
||||
try {
|
||||
executeLifecycle(request, runImageReference, builder);
|
||||
}
|
||||
finally {
|
||||
this.docker.image().remove(builder.getName(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private Image pullBuilder(BuildRequest request) throws IOException {
|
||||
ImageReference builderImageReference = request.getBuilder();
|
||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingBuilder(request, builderImageReference);
|
||||
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
|
||||
Image builderImage = this.docker.image().pull(builderImageReference, listener);
|
||||
this.log.pulledBulder(request, builderImage);
|
||||
return builderImage;
|
||||
}
|
||||
|
||||
private ImageReference getRunImageReference(Stack stack) {
|
||||
String name = stack.getRunImage().getImage();
|
||||
Assert.state(StringUtils.hasText(name), "Run image must be specified");
|
||||
return ImageReference.of(name);
|
||||
}
|
||||
|
||||
private Image pullRunImage(BuildRequest request, ImageReference name) throws IOException {
|
||||
Consumer<TotalProgressEvent> progressConsumer = this.log.pullingRunImage(request, name);
|
||||
TotalProgressPullListener listener = new TotalProgressPullListener(progressConsumer);
|
||||
Image image = this.docker.image().pull(name, listener);
|
||||
this.log.pulledRunImage(request, image);
|
||||
return image;
|
||||
}
|
||||
|
||||
private void assertHasExpectedStackId(Image image, StackId stackId) {
|
||||
StackId pulledStackId = StackId.fromImage(image);
|
||||
Assert.state(pulledStackId.equals(stackId),
|
||||
"Run image stack '" + pulledStackId + "' does not match builder stack '" + stackId + "'");
|
||||
}
|
||||
|
||||
private void executeLifecycle(BuildRequest request, ImageReference runImageReference, EphemeralBuilder builder)
|
||||
throws IOException {
|
||||
try (Lifecycle lifecycle = new Lifecycle(this.log, this.docker, request, runImageReference, builder)) {
|
||||
lifecycle.execute();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,263 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.MappedObject;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Builder metadata information.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuilderMetadata extends MappedObject {
|
||||
|
||||
private static final String LABEL_NAME = "io.buildpacks.builder.metadata";
|
||||
|
||||
private static final String[] EMPTY_MIRRORS = {};
|
||||
|
||||
private final Stack stack;
|
||||
|
||||
private final Lifecycle lifecycle;
|
||||
|
||||
private final CreatedBy createdBy;
|
||||
|
||||
BuilderMetadata(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.stack = valueAt("/stack", Stack.class);
|
||||
this.lifecycle = valueAt("/lifecycle", Lifecycle.class);
|
||||
this.createdBy = valueAt("/createdBy", CreatedBy.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return stack metadata.
|
||||
* @return the stack metadata
|
||||
*/
|
||||
Stack getStack() {
|
||||
return this.stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return lifecycle metadata.
|
||||
* @return the lifecycle metadata
|
||||
*/
|
||||
Lifecycle getLifecycle() {
|
||||
return this.lifecycle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return information about who created the builder.
|
||||
* @return the created by metadata
|
||||
*/
|
||||
CreatedBy getCreatedBy() {
|
||||
return this.createdBy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an updated copy of this metadata.
|
||||
* @param update consumer to apply updates
|
||||
* @return an updated metadata instance
|
||||
*/
|
||||
BuilderMetadata copy(Consumer<Update> update) {
|
||||
return new Update(this).run(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach this metadata to the given update callback.
|
||||
* @param update the update used to attach the metadata
|
||||
*/
|
||||
void attachTo(ImageConfig.Update update) {
|
||||
try {
|
||||
String json = SharedObjectMapper.get().writeValueAsString(getNode());
|
||||
update.withLabel(LABEL_NAME, json);
|
||||
}
|
||||
catch (JsonProcessingException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to extract {@link BuilderMetadata} from an image.
|
||||
* @param image the source image
|
||||
* @return the builder metadata
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
static BuilderMetadata fromImage(Image image) throws IOException {
|
||||
Assert.notNull(image, "Image must not be null");
|
||||
return fromImageConfig(image.getConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to extract {@link BuilderMetadata} from image config.
|
||||
* @param imageConfig the image config
|
||||
* @return the builder metadata
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
static BuilderMetadata fromImageConfig(ImageConfig imageConfig) throws IOException {
|
||||
Assert.notNull(imageConfig, "ImageConfig must not be null");
|
||||
Map<String, String> labels = imageConfig.getLabels();
|
||||
String json = (labels != null) ? labels.get(LABEL_NAME) : null;
|
||||
Assert.notNull(json, "No '" + LABEL_NAME + "' label found in image config");
|
||||
return fromJson(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method create {@link BuilderMetadata} from some JSON.
|
||||
* @param json the source JSON
|
||||
* @return the builder metadata
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
static BuilderMetadata fromJson(String json) throws IOException {
|
||||
return new BuilderMetadata(SharedObjectMapper.get().readTree(json));
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack metadata.
|
||||
*/
|
||||
interface Stack {
|
||||
|
||||
/**
|
||||
* Return run image metadata.
|
||||
* @return the run image metadata
|
||||
*/
|
||||
RunImage getRunImage();
|
||||
|
||||
/**
|
||||
* Run image metadata.
|
||||
*/
|
||||
interface RunImage {
|
||||
|
||||
/**
|
||||
* Return the builder image reference.
|
||||
* @return the image reference
|
||||
*/
|
||||
String getImage();
|
||||
|
||||
/**
|
||||
* Return stack mirrors.
|
||||
* @return the stack mirrors
|
||||
*/
|
||||
default String[] getMirrors() {
|
||||
return EMPTY_MIRRORS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle metadata.
|
||||
*/
|
||||
interface Lifecycle {
|
||||
|
||||
/**
|
||||
* Return the lifecycle version.
|
||||
* @return the lifecycle version
|
||||
*/
|
||||
String getVersion();
|
||||
|
||||
/**
|
||||
* Return the API versions.
|
||||
* @return the API versions
|
||||
*/
|
||||
Api getApi();
|
||||
|
||||
/**
|
||||
* API versions.
|
||||
*/
|
||||
interface Api {
|
||||
|
||||
/**
|
||||
* Return the buildpack API version.
|
||||
* @return the buildpack version
|
||||
*/
|
||||
String getBuildpack();
|
||||
|
||||
/**
|
||||
* Return the platform API version.
|
||||
* @return the platform version
|
||||
*/
|
||||
String getPlatform();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Created-by metadata.
|
||||
*/
|
||||
interface CreatedBy {
|
||||
|
||||
/**
|
||||
* Return the name of the creator.
|
||||
* @return the creator name
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Return the version of the creator.
|
||||
* @return the creator version
|
||||
*/
|
||||
String getVersion();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update class used to change data when creating a copy.
|
||||
*/
|
||||
static final class Update {
|
||||
|
||||
private ObjectNode copy;
|
||||
|
||||
private Update(BuilderMetadata source) {
|
||||
this.copy = source.getNode().deepCopy();
|
||||
}
|
||||
|
||||
private BuilderMetadata run(Consumer<Update> update) {
|
||||
update.accept(this);
|
||||
return new BuilderMetadata(this.copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the builder meta-data with a specific created by section.
|
||||
* @param name the name of the creator
|
||||
* @param version the version of the creator
|
||||
*/
|
||||
void withCreatedBy(String name, String version) {
|
||||
ObjectNode createdBy = (ObjectNode) this.copy.at("/createdBy");
|
||||
if (createdBy == null) {
|
||||
createdBy = this.copy.putObject("createdBy");
|
||||
}
|
||||
createdBy.put("name", name);
|
||||
createdBy.put("version", version);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.build.BuilderMetadata.Stack.RunImage;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Layer;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Content;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.boot.cloudnativebuildpack.toml.Toml;
|
||||
|
||||
/**
|
||||
* An short lived builder that is created for each {@link Lifecycle} run.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class EphemeralBuilder {
|
||||
|
||||
private final BuildOwner buildOwner;
|
||||
|
||||
private final BuilderMetadata builderMetadata;
|
||||
|
||||
private final ImageArchive archive;
|
||||
|
||||
/**
|
||||
* Create a new {@link EphemeralBuilder} instance.
|
||||
* @param buildOwner the build owner
|
||||
* @param builderImage the image
|
||||
* @param builderMetadata the builder metadata
|
||||
* @param env the builder env
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
EphemeralBuilder(BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata,
|
||||
Map<String, String> env) throws IOException {
|
||||
this(Clock.systemUTC(), buildOwner, builderImage, builderMetadata, env);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link EphemeralBuilder} instance with a specific clock.
|
||||
* @param clock the clock used for the current time
|
||||
* @param buildOwner the build owner
|
||||
* @param builderImage the image
|
||||
* @param builderMetadata the builder metadata
|
||||
* @param env the builder env
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
EphemeralBuilder(Clock clock, BuildOwner buildOwner, Image builderImage, BuilderMetadata builderMetadata,
|
||||
Map<String, String> env) throws IOException {
|
||||
ImageReference name = ImageReference.random("pack.local/builder/").inTaggedForm();
|
||||
this.buildOwner = buildOwner;
|
||||
this.builderMetadata = builderMetadata.copy(this::updateMetadata);
|
||||
this.archive = ImageArchive.from(builderImage, (update) -> {
|
||||
update.withUpdatedConfig(this.builderMetadata::attachTo);
|
||||
update.withTag(name);
|
||||
update.withCreateDate(Instant.now(clock));
|
||||
update.withNewLayer(getDefaultDirsLayer(buildOwner));
|
||||
update.withNewLayer(getStackLayer(builderMetadata));
|
||||
if (env != null && !env.isEmpty()) {
|
||||
update.withNewLayer(getEnvLayer(env));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMetadata(BuilderMetadata.Update update) {
|
||||
update.withCreatedBy("Spring Boot", "dev");
|
||||
}
|
||||
|
||||
private Layer getDefaultDirsLayer(Owner buildOwner) throws IOException {
|
||||
return Layer.of((layout) -> {
|
||||
layout.folder("/workspace", buildOwner);
|
||||
layout.folder("/layers", buildOwner);
|
||||
layout.folder("/cnb", Owner.ROOT);
|
||||
layout.folder("/cnb/buildpacks", Owner.ROOT);
|
||||
layout.folder("/platform", Owner.ROOT);
|
||||
layout.folder("/platform/env", Owner.ROOT);
|
||||
});
|
||||
}
|
||||
|
||||
private Layer getStackLayer(BuilderMetadata builderMetadata) throws IOException {
|
||||
Toml toml = getRunImageToml(builderMetadata.getStack().getRunImage());
|
||||
return Layer.of((layout) -> layout.file("/cnb/stack.toml", Owner.ROOT, Content.of(toml.toString())));
|
||||
}
|
||||
|
||||
private Toml getRunImageToml(RunImage runImage) {
|
||||
Toml toml = new Toml();
|
||||
toml.table("run-image");
|
||||
toml.string("image", runImage.getImage());
|
||||
toml.array("mirrors", runImage.getMirrors());
|
||||
return toml;
|
||||
}
|
||||
|
||||
private Layer getEnvLayer(Map<String, String> env) throws IOException {
|
||||
return Layer.of((layout) -> {
|
||||
for (Map.Entry<String, String> entry : env.entrySet()) {
|
||||
String name = "/platform/env/" + entry.getKey();
|
||||
Content content = Content.of(entry.getValue());
|
||||
layout.file(name, Owner.ROOT, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of this archive as tagged in Docker.
|
||||
* @return the ephemeral builder name
|
||||
*/
|
||||
ImageReference getName() {
|
||||
return this.archive.getTag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the build owner that should be used for written content.
|
||||
* @return the builder owner
|
||||
*/
|
||||
Owner getBuildOwner() {
|
||||
return this.buildOwner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the builder meta-data that was used to create this ephemeral builder.
|
||||
* @return the builder meta-data
|
||||
*/
|
||||
BuilderMetadata getBuilderMetadata() {
|
||||
return this.builderMetadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of ephemeral builder for passing to Docker.
|
||||
* @return the ephemeral builder archive
|
||||
*/
|
||||
ImageArchive getArchive() {
|
||||
return this.archive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.archive.getTag().toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,300 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A buildpack lifecycle used to run the build {@link Phase phases} needed to package an
|
||||
* application.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class Lifecycle implements Closeable {
|
||||
|
||||
private static final LifecycleVersion LOGGING_SUPPORTED_VERSION = LifecycleVersion.parse("0.0.5");
|
||||
|
||||
private final BuildLog log;
|
||||
|
||||
private final DockerApi docker;
|
||||
|
||||
private final BuildRequest request;
|
||||
|
||||
private final ImageReference runImageReference;
|
||||
|
||||
private final EphemeralBuilder builder;
|
||||
|
||||
private final LifecycleVersion version;
|
||||
|
||||
private final VolumeName layersVolume;
|
||||
|
||||
private final VolumeName applicationVolume;
|
||||
|
||||
private final VolumeName buildCacheVolume;
|
||||
|
||||
private final VolumeName launchCacheVolume;
|
||||
|
||||
private boolean executed;
|
||||
|
||||
private boolean applicationVolumePopulated;
|
||||
|
||||
/**
|
||||
* Create a new {@link Lifecycle} instance.
|
||||
* @param log build output log
|
||||
* @param docker the Docker API
|
||||
* @param request the request to process
|
||||
* @param runImageReferece a reference to run image that should be used
|
||||
* @param builder the ephemeral builder used to run the phases
|
||||
*/
|
||||
Lifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReferece,
|
||||
EphemeralBuilder builder) {
|
||||
checkPlatformVersion(builder);
|
||||
this.log = log;
|
||||
this.docker = docker;
|
||||
this.request = request;
|
||||
this.runImageReference = runImageReferece;
|
||||
this.builder = builder;
|
||||
this.version = LifecycleVersion.parse(builder.getBuilderMetadata().getLifecycle().getVersion());
|
||||
this.layersVolume = createRandomVolumeName("pack-layers-");
|
||||
this.applicationVolume = createRandomVolumeName("pack-app-");
|
||||
this.buildCacheVolume = createCacheVolumeName(request, ".build");
|
||||
this.launchCacheVolume = createCacheVolumeName(request, ".launch");
|
||||
}
|
||||
|
||||
protected VolumeName createRandomVolumeName(String prefix) {
|
||||
return VolumeName.random(prefix);
|
||||
}
|
||||
|
||||
private VolumeName createCacheVolumeName(BuildRequest request, String suffix) {
|
||||
return VolumeName.basedOn(request.getName(), ImageReference::toLegacyString, "pack-cache-", suffix, 6);
|
||||
}
|
||||
|
||||
private void checkPlatformVersion(EphemeralBuilder ephemeralBuilder) {
|
||||
String platformVersion = ephemeralBuilder.getBuilderMetadata().getLifecycle().getApi().getPlatform();
|
||||
if (StringUtils.hasText(platformVersion)) {
|
||||
ApiVersion.PLATFORM.assertSupports(ApiVersion.parse(platformVersion));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute this lifecycle by running each phase in turn.
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void execute() throws IOException {
|
||||
Assert.state(!this.executed, "Lifecycle has already been executed");
|
||||
this.executed = true;
|
||||
this.log.executingLifecycle(this.request, this.version, this.buildCacheVolume);
|
||||
if (this.request.isCleanCache()) {
|
||||
deleteVolume(this.buildCacheVolume);
|
||||
}
|
||||
run(detectPhase());
|
||||
run(restorePhase());
|
||||
run(analyzePhase());
|
||||
run(buildPhase());
|
||||
run(exportPhase());
|
||||
run(cachePhase());
|
||||
this.log.executedLifecycle(this.request);
|
||||
}
|
||||
|
||||
private Phase detectPhase() {
|
||||
Phase phase = createPhase("detector");
|
||||
phase.withArgs("-app", Folder.APPLICATION);
|
||||
phase.withArgs("-platform", Folder.PLATFORM);
|
||||
phase.withLogLevelArg();
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase restorePhase() {
|
||||
Phase phase = createPhase("restorer");
|
||||
phase.withDaemonAccess();
|
||||
phase.withArgs("-path", Folder.CACHE);
|
||||
phase.withArgs("-layers", Folder.LAYERS);
|
||||
phase.withLogLevelArg();
|
||||
phase.withBinds(this.buildCacheVolume, Folder.CACHE);
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase analyzePhase() {
|
||||
Phase phase = createPhase("analyzer");
|
||||
phase.withDaemonAccess();
|
||||
phase.withLogLevelArg();
|
||||
if (this.request.isCleanCache()) {
|
||||
phase.withArgs("-skip-layers");
|
||||
}
|
||||
phase.withArgs("-daemon");
|
||||
phase.withArgs("-layers", Folder.LAYERS);
|
||||
phase.withArgs(this.request.getName());
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase buildPhase() {
|
||||
Phase phase = createPhase("builder");
|
||||
phase.withArgs("-layers", Folder.LAYERS);
|
||||
phase.withArgs("-app", Folder.APPLICATION);
|
||||
phase.withArgs("-platform", Folder.PLATFORM);
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase exportPhase() {
|
||||
Phase phase = createPhase("exporter");
|
||||
phase.withDaemonAccess();
|
||||
phase.withLogLevelArg();
|
||||
phase.withArgs("-image", this.runImageReference);
|
||||
phase.withArgs("-layers", Folder.LAYERS);
|
||||
phase.withArgs("-app", Folder.APPLICATION);
|
||||
phase.withArgs("-daemon");
|
||||
phase.withArgs("-launch-cache", Folder.LAUNCH_CACHE);
|
||||
phase.withArgs(this.request.getName());
|
||||
phase.withBinds(this.launchCacheVolume, Folder.LAUNCH_CACHE);
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase cachePhase() {
|
||||
Phase phase = createPhase("cacher");
|
||||
phase.withDaemonAccess();
|
||||
phase.withArgs("-path", Folder.CACHE);
|
||||
phase.withArgs("-layers", Folder.LAYERS);
|
||||
phase.withLogLevelArg();
|
||||
phase.withBinds(this.buildCacheVolume, Folder.CACHE);
|
||||
return phase;
|
||||
}
|
||||
|
||||
private Phase createPhase(String name) {
|
||||
boolean verboseLogging = this.request.isVerboseLogging()
|
||||
&& this.version.isEqualOrGreaterThan(LOGGING_SUPPORTED_VERSION);
|
||||
Phase phase = new Phase(name, verboseLogging);
|
||||
phase.withBinds(this.layersVolume, Folder.LAYERS);
|
||||
phase.withBinds(this.applicationVolume, Folder.APPLICATION);
|
||||
return phase;
|
||||
}
|
||||
|
||||
private void run(Phase phase) throws IOException {
|
||||
Consumer<LogUpdateEvent> logConsumer = this.log.runningPhase(this.request, phase.getName());
|
||||
ContainerConfig containerConfig = ContainerConfig.of(this.builder.getName(), phase::apply);
|
||||
ContainerReference reference = createContainer(containerConfig);
|
||||
try {
|
||||
this.docker.container().start(reference);
|
||||
this.docker.container().logs(reference, logConsumer::accept);
|
||||
}
|
||||
finally {
|
||||
this.docker.container().remove(reference, true);
|
||||
}
|
||||
}
|
||||
|
||||
private ContainerReference createContainer(ContainerConfig config) throws IOException {
|
||||
if (this.applicationVolumePopulated) {
|
||||
return this.docker.container().create(config);
|
||||
}
|
||||
try {
|
||||
TarArchive applicationContent = this.request.getApplicationContent(this.builder.getBuildOwner());
|
||||
return this.docker.container().create(config, ContainerContent.of(applicationContent, Folder.APPLICATION));
|
||||
}
|
||||
finally {
|
||||
this.applicationVolumePopulated = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
deleteVolume(this.layersVolume);
|
||||
deleteVolume(this.applicationVolume);
|
||||
}
|
||||
|
||||
private void deleteVolume(VolumeName name) throws IOException {
|
||||
this.docker.volume().delete(name, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common folders used by the various phases.
|
||||
*/
|
||||
private static class Folder {
|
||||
|
||||
/**
|
||||
* The folder used by buildpacks to write their layer contributions. A new layer
|
||||
* folder is created for each lifecycle execution.
|
||||
* <p>
|
||||
* Maps to the {@code <layers...>} concept in the
|
||||
* <a href="https://github.com/buildpacks/spec/blob/master/buildpack.md">buildpack
|
||||
* specification</a> and the {@code -layers} argument from the reference lifecycle
|
||||
* implementation.
|
||||
*/
|
||||
static final String LAYERS = "/layers";
|
||||
|
||||
/**
|
||||
* The folder containing the original contributed application. A new application
|
||||
* folder is created for each lifecycle execution.
|
||||
* <p>
|
||||
* Maps to the {@code <app...>} concept in the
|
||||
* <a href="https://github.com/buildpacks/spec/blob/master/buildpack.md">buildpack
|
||||
* specification</a> and the {@code -app} argument from the reference lifecycle
|
||||
* implementation. The reference lifecycle follows the Kubernetes/Docker
|
||||
* convention of using {@code '/workspace'}.
|
||||
* <p>
|
||||
* Note that application content is uploaded to the container with the first phase
|
||||
* that runs and saved in a volume that is passed to supsequent phases. The folder
|
||||
* is mutable and buildpacks may modify the content.
|
||||
*/
|
||||
static final String APPLICATION = "/workspace";
|
||||
|
||||
/**
|
||||
* The folder used by buildpacks to obtain environment variables and platform
|
||||
* specific concerns. The platform folder is read-only and is created/populated by
|
||||
* the {@link EphemeralBuilder}.
|
||||
* <p>
|
||||
* Maps to the {@code <platform>/env} and {@code <platform>/#} concepts in the
|
||||
* <a href="https://github.com/buildpacks/spec/blob/master/buildpack.md">buildpack
|
||||
* specification</a> and the {@code -platform} argument from the reference
|
||||
* lifecycle implementation.
|
||||
*/
|
||||
static final String PLATFORM = "/platform";
|
||||
|
||||
/**
|
||||
* The folder used by buildpacks for caching. The volume name is based on the
|
||||
* image {@link BuildRequest#getName() name} being built, and is persistent across
|
||||
* invocations even if the application content has changed.
|
||||
* <p>
|
||||
* Maps to the {@code -path} argument from the reference lifecycle implementation
|
||||
* cache and restore phases
|
||||
*/
|
||||
static final String CACHE = "/cache";
|
||||
|
||||
/**
|
||||
* The folder used by buildpacks for launch related caching. The volume name is
|
||||
* based on the image {@link BuildRequest#getName() name} being built, and is
|
||||
* persistent across invocations even if the application content has changed.
|
||||
* <p>
|
||||
* Maps to the {@code -launch-cache} argument from the reference lifecycle
|
||||
* implementation export phase
|
||||
*/
|
||||
static final String LAUNCH_CACHE = "/launch-cache";
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.Comparator;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A lifecycle version number comprised of a major, minor and patch value.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LifecycleVersion implements Comparable<LifecycleVersion> {
|
||||
|
||||
private static final Comparator<LifecycleVersion> COMPARATOR = Comparator.comparingInt(LifecycleVersion::getMajor)
|
||||
.thenComparingInt(LifecycleVersion::getMinor).thenComparing(LifecycleVersion::getPatch);
|
||||
|
||||
private final int major;
|
||||
|
||||
private final int minor;
|
||||
|
||||
private final int patch;
|
||||
|
||||
LifecycleVersion(int major, int minor, int patch) {
|
||||
this.major = major;
|
||||
this.minor = minor;
|
||||
this.patch = patch;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
LifecycleVersion other = (LifecycleVersion) obj;
|
||||
boolean result = true;
|
||||
result = result && this.major == other.major;
|
||||
result = result && this.minor == other.minor;
|
||||
result = result && this.patch == other.patch;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.major;
|
||||
result = prime * result + this.minor;
|
||||
result = prime * result + this.patch;
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "v" + this.major + "." + this.minor + "." + this.patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if this version is greater than or equal to the specified version.
|
||||
* @param other the version to compare
|
||||
* @return {@code true} if this version is greater than or equal to the specified
|
||||
* version
|
||||
*/
|
||||
boolean isEqualOrGreaterThan(LifecycleVersion other) {
|
||||
return this.compareTo(other) >= 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(LifecycleVersion other) {
|
||||
return COMPARATOR.compare(this, other);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the major version number.
|
||||
* @return the major version
|
||||
*/
|
||||
int getMajor() {
|
||||
return this.major;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the minor version number.
|
||||
* @return the minor version
|
||||
*/
|
||||
int getMinor() {
|
||||
return this.minor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the patch version number.
|
||||
* @return the patch version
|
||||
*/
|
||||
int getPatch() {
|
||||
return this.patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to parse a string into a {@link LifecycleVersion} instance.
|
||||
* @param value the value to parse.
|
||||
* @return the corresponding {@link LifecycleVersion}
|
||||
* @throws IllegalArgumentException if the value could not be parsed
|
||||
*/
|
||||
static LifecycleVersion parse(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
if (value.startsWith("v") || value.startsWith("V")) {
|
||||
value = value.substring(1);
|
||||
}
|
||||
String[] components = value.split("\\.");
|
||||
Assert.isTrue(components.length <= 3, "Malformed version number '" + value + "'");
|
||||
int[] versions = new int[3];
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
try {
|
||||
versions[i] = Integer.parseInt(components[i]);
|
||||
}
|
||||
catch (NumberFormatException ex) {
|
||||
throw new IllegalArgumentException("Malformed version number '" + value + "'", ex);
|
||||
}
|
||||
}
|
||||
return new LifecycleVersion(versions[0], versions[1], versions[2]);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* An individual build phase executed as part of a {@link Lifecycle} run.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class Phase {
|
||||
|
||||
private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock";
|
||||
|
||||
private final String name;
|
||||
|
||||
private final boolean verboseLogging;
|
||||
|
||||
private boolean daemonAccess = false;
|
||||
|
||||
private final List<String> args = new ArrayList<>();
|
||||
|
||||
private final Map<VolumeName, String> binds = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Create a new {@link Phase} instance.
|
||||
* @param name the name of the phase
|
||||
* @param verboseLogging if verbose logging is requested
|
||||
*/
|
||||
Phase(String name, boolean verboseLogging) {
|
||||
this.name = name;
|
||||
this.verboseLogging = verboseLogging;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this phase with Docker daemon access.
|
||||
*/
|
||||
void withDaemonAccess() {
|
||||
this.daemonAccess = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this phase with a debug log level arguments if verbose logging has been
|
||||
* requested.
|
||||
*/
|
||||
void withLogLevelArg() {
|
||||
if (this.verboseLogging) {
|
||||
this.args.add("-log-level");
|
||||
this.args.add("debug");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this phase with additional run arguments.
|
||||
* @param args the arguments to add
|
||||
*/
|
||||
void withArgs(Object... args) {
|
||||
Arrays.stream(args).map(Object::toString).forEach(this.args::add);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update this phase with an addition volume binding.
|
||||
* @param source the source volume
|
||||
* @param dest the destination location
|
||||
*/
|
||||
void withBinds(VolumeName source, String dest) {
|
||||
this.binds.put(source, dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of the phase.
|
||||
* @return the phase name
|
||||
*/
|
||||
String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply this phase settings to a {@link ContainerConfig} update.
|
||||
* @param update the update to apply the phase to
|
||||
*/
|
||||
void apply(ContainerConfig.Update update) {
|
||||
if (this.daemonAccess) {
|
||||
update.withUser("root");
|
||||
update.withBind(DOMAIN_SOCKET_PATH, DOMAIN_SOCKET_PATH);
|
||||
}
|
||||
update.withCommand("/lifecycle/" + this.name, StringUtils.toStringArray(this.args));
|
||||
update.withLabel("author", "spring-boot");
|
||||
this.binds.forEach((source, dest) -> update.withBind(source, dest));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressBar;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
|
||||
|
||||
/**
|
||||
* {@link BuildLog} implementation that prints output to a {@link PrintStream}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see BuildLog#to(PrintStream)
|
||||
*/
|
||||
class PrintStreamBuildLog extends AbstractBuildLog {
|
||||
|
||||
private final PrintStream out;
|
||||
|
||||
PrintStreamBuildLog(PrintStream out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void log(String message) {
|
||||
this.out.println(message);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Consumer<TotalProgressEvent> getProgressConsumer(String prefix) {
|
||||
return new TotalProgressBar(prefix, '.', false, this.out);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A Stack ID.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StackId {
|
||||
|
||||
private static final String LABEL_NAME = "io.buildpacks.stack.id";
|
||||
|
||||
private final String value;
|
||||
|
||||
StackId(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return this.value.equals(((StackId) obj).value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.value.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link StackId} from an {@link Image}.
|
||||
* @param image the source image
|
||||
* @return the extracted stack ID
|
||||
*/
|
||||
static StackId fromImage(Image image) {
|
||||
Assert.notNull(image, "Image must not be null");
|
||||
return fromImageConfig(image.getConfig());
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link StackId} from an {@link ImageConfig}.
|
||||
* @param imageConfig the source image config
|
||||
* @return the extracted stack ID
|
||||
*/
|
||||
private static StackId fromImageConfig(ImageConfig imageConfig) {
|
||||
Map<String, String> labels = imageConfig.getLabels();
|
||||
String value = (labels != null) ? labels.get(LABEL_NAME) : null;
|
||||
Assert.state(StringUtils.hasText(value), "Missing '" + LABEL_NAME + "' stack label");
|
||||
return new StackId(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link StackId} with a given value.
|
||||
* @param value the stack ID value
|
||||
* @return a new stack ID instance
|
||||
*/
|
||||
static StackId of(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
return new StackId(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Central API for performing a buildpack build.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.http.client.utils.URIBuilder;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.Http.Response;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.JsonStream;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Provides access to the limited set of Docker APIs needed by pack.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class DockerApi {
|
||||
|
||||
private static final List<String> FORCE_PARAMS = Collections.unmodifiableList(Arrays.asList("force", "1"));
|
||||
|
||||
private final Http http;
|
||||
|
||||
private final JsonStream jsonStream;
|
||||
|
||||
private final ImageApi image;
|
||||
|
||||
private final ContainerApi container;
|
||||
|
||||
private final VolumeApi volume;
|
||||
|
||||
/**
|
||||
* Create a new {@link DockerApi} instance.
|
||||
*/
|
||||
public DockerApi() {
|
||||
this(new HttpClientHttp());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link DockerApi} instance backed by a specific {@link HttpClientHttp}
|
||||
* implementation.
|
||||
* @param http the http implementation
|
||||
*/
|
||||
DockerApi(Http http) {
|
||||
this.http = http;
|
||||
this.jsonStream = new JsonStream(SharedObjectMapper.get());
|
||||
this.image = new ImageApi();
|
||||
this.container = new ContainerApi();
|
||||
this.volume = new VolumeApi();
|
||||
}
|
||||
|
||||
private Http http() {
|
||||
return this.http;
|
||||
}
|
||||
|
||||
private JsonStream jsonStream() {
|
||||
return this.jsonStream;
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, Collection<String> params) {
|
||||
return buildUrl(path, StringUtils.toStringArray(params));
|
||||
}
|
||||
|
||||
private URI buildUrl(String path, String... params) {
|
||||
try {
|
||||
URIBuilder builder = new URIBuilder("docker://localhost/v1.40" + path);
|
||||
int param = 0;
|
||||
while (param < params.length) {
|
||||
builder.addParameter(params[param++], params[param++]);
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Docker API for image operations.
|
||||
* @return the image API
|
||||
*/
|
||||
public ImageApi image() {
|
||||
return this.image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Docker API for container operations.
|
||||
* @return the container API
|
||||
*/
|
||||
public ContainerApi container() {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
public VolumeApi volume() {
|
||||
return this.volume;
|
||||
}
|
||||
|
||||
/**
|
||||
* Docker API for image operations.
|
||||
*/
|
||||
public class ImageApi {
|
||||
|
||||
ImageApi() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull an image from a registry.
|
||||
* @param reference the image reference to pull
|
||||
* @param listener a pull listener to receive update events
|
||||
* @return the {@link ImageApi pulled image} instance
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public Image pull(ImageReference reference, UpdateListener<PullImageUpdateEvent> listener) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Assert.notNull(listener, "Listener must not be null");
|
||||
URI createUri = buildUrl("/images/create", "fromImage", reference.toString());
|
||||
DigestCaptureUpdateListener digestCapture = new DigestCaptureUpdateListener();
|
||||
listener.onStart();
|
||||
try {
|
||||
try (Response response = http().post(createUri)) {
|
||||
jsonStream().get(response.getContent(), PullImageUpdateEvent.class, (event) -> {
|
||||
digestCapture.onUpdate(event);
|
||||
listener.onUpdate(event);
|
||||
});
|
||||
}
|
||||
URI imageUri = buildUrl("/images/" + reference.withDigest(digestCapture.getCapturedDigest()) + "/json");
|
||||
try (Response response = http().get(imageUri)) {
|
||||
return Image.of(response.getContent());
|
||||
}
|
||||
}
|
||||
finally {
|
||||
listener.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load an {@link ImageArchive} into Docker.
|
||||
* @param archive the archive to load
|
||||
* @param listener a pull listener to receive update events
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void load(ImageArchive archive, UpdateListener<LoadImageUpdateEvent> listener) throws IOException {
|
||||
Assert.notNull(archive, "Archive must not be null");
|
||||
Assert.notNull(listener, "Listener must not be null");
|
||||
URI loadUri = buildUrl("/images/load");
|
||||
listener.onStart();
|
||||
try {
|
||||
try (Response response = http().post(loadUri, "application/x-tar", archive::writeTo)) {
|
||||
jsonStream().get(response.getContent(), LoadImageUpdateEvent.class, listener::onUpdate);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
listener.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific image.
|
||||
* @param reference the reference the remove
|
||||
* @param force if removal should be forced
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void remove(ImageReference reference, boolean force) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/images/" + reference, params);
|
||||
http().delete(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Docker API for container operations.
|
||||
*/
|
||||
public class ContainerApi {
|
||||
|
||||
ContainerApi() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new container a {@link ContainerConfig}.
|
||||
* @param config the container config
|
||||
* @param contents additional contents to include
|
||||
* @return a {@link ContainerReference} for the newly created container
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public ContainerReference create(ContainerConfig config, ContainerContent... contents) throws IOException {
|
||||
Assert.notNull(config, "Config must not be null");
|
||||
Assert.noNullElements(contents, "Contents must not contain null elements");
|
||||
ContainerReference containerReference = createContainer(config);
|
||||
for (ContainerContent content : contents) {
|
||||
uploadContainerContent(containerReference, content);
|
||||
}
|
||||
return containerReference;
|
||||
}
|
||||
|
||||
private ContainerReference createContainer(ContainerConfig config) throws IOException {
|
||||
URI createUri = buildUrl("/containers/create");
|
||||
try (Response response = http().post(createUri, "application/json", config::writeTo)) {
|
||||
ContainerReference containerReference = ContainerReference
|
||||
.of(SharedObjectMapper.get().readTree(response.getContent()).at("/Id").asText());
|
||||
return containerReference;
|
||||
}
|
||||
}
|
||||
|
||||
private void uploadContainerContent(ContainerReference reference, ContainerContent content) throws IOException {
|
||||
URI uri = buildUrl("/containers/" + reference + "/archive", "path", content.getDestinationPath());
|
||||
http().put(uri, "application/x-tar", content.getArchive()::writeTo).close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a specific container.
|
||||
* @param reference the container reference to start
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void start(ContainerReference reference) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
URI uri = buildUrl("/containers/" + reference + "/start");
|
||||
http().post(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return and follow logs for a specific container.
|
||||
* @param reference the container reference
|
||||
* @param listener a listener to receive log update events
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void logs(ContainerReference reference, UpdateListener<LogUpdateEvent> listener) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Assert.notNull(listener, "Listener must not be null");
|
||||
String[] params = { "stdout", "1", "stderr", "1", "follow", "1" };
|
||||
URI uri = buildUrl("/containers/" + reference + "/logs", params);
|
||||
listener.onStart();
|
||||
try {
|
||||
try (Response response = http().get(uri)) {
|
||||
LogUpdateEvent.readAll(response.getContent(), listener::onUpdate);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
listener.onFinish();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a specific container.
|
||||
* @param reference the container to remove
|
||||
* @param force if removal should be forced
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void remove(ContainerReference reference, boolean force) throws IOException {
|
||||
Assert.notNull(reference, "Reference must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/containers/" + reference, params);
|
||||
http().delete(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Docker API for volume operations.
|
||||
*/
|
||||
public class VolumeApi {
|
||||
|
||||
VolumeApi() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a volume.
|
||||
* @param name the name of the volume to delete
|
||||
* @param force if the deletion should be forced
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void delete(VolumeName name, boolean force) throws IOException {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
Collection<String> params = force ? FORCE_PARAMS : Collections.emptySet();
|
||||
URI uri = buildUrl("/volumes/" + name, params);
|
||||
http().delete(uri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link UpdateListener} used to capture the image digest.
|
||||
*/
|
||||
private static class DigestCaptureUpdateListener implements UpdateListener<ProgressUpdateEvent> {
|
||||
|
||||
private static final String PREFIX = "Digest:";
|
||||
|
||||
private String digest;
|
||||
|
||||
@Override
|
||||
public void onUpdate(ProgressUpdateEvent event) {
|
||||
String status = event.getStatus();
|
||||
if (status != null && status.startsWith(PREFIX)) {
|
||||
String digest = status.substring(PREFIX.length()).trim();
|
||||
Assert.state(this.digest == null || this.digest.equals(digest), "Different digests IDs provided");
|
||||
this.digest = digest;
|
||||
}
|
||||
}
|
||||
|
||||
String getCapturedDigest() {
|
||||
Assert.hasText(this.digest, "No digest found");
|
||||
return this.digest;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
|
||||
import com.sun.jna.Platform;
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.protocol.HttpContext;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.socket.DomainSocket;
|
||||
import org.springframework.boot.cloudnativebuildpack.socket.NamedPipeSocket;
|
||||
|
||||
/**
|
||||
* {@link ConnectionSocketFactory} that connects to the Docker domain socket or named
|
||||
* pipe.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerConnectionSocketFactory implements ConnectionSocketFactory {
|
||||
|
||||
private static final String DOMAIN_SOCKET_PATH = "/var/run/docker.sock";
|
||||
|
||||
private static final String WINDOWS_NAMED_PIPE_PATH = "//./pipe/docker_engine";
|
||||
|
||||
@Override
|
||||
public Socket createSocket(HttpContext context) throws IOException {
|
||||
if (Platform.isWindows()) {
|
||||
NamedPipeSocket.get(WINDOWS_NAMED_PIPE_PATH);
|
||||
}
|
||||
return DomainSocket.get(DOMAIN_SOCKET_PATH);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Socket connectSocket(int connectTimeout, Socket sock, HttpHost host, InetSocketAddress remoteAddress,
|
||||
InetSocketAddress localAddress, HttpContext context) throws IOException {
|
||||
return sock;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
import org.apache.http.conn.DnsResolver;
|
||||
|
||||
/**
|
||||
* {@link DnsResolver} used by the {@link DockerHttpClientConnectionManager} to ensure
|
||||
* only the loopback address is used.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerDnsResolver implements DnsResolver {
|
||||
|
||||
private static final InetAddress[] LOOPBACK = new InetAddress[] { InetAddress.getLoopbackAddress() };
|
||||
|
||||
@Override
|
||||
public InetAddress[] resolve(String host) throws UnknownHostException {
|
||||
return LOOPBACK;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Exception throw when the Docker API fails.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class DockerException extends RuntimeException {
|
||||
|
||||
private final int statusCode;
|
||||
|
||||
private final String reasonPhrase;
|
||||
|
||||
private final Errors errors;
|
||||
|
||||
DockerException(URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
super(buildMessage(uri, statusCode, reasonPhrase, errors));
|
||||
this.statusCode = statusCode;
|
||||
this.reasonPhrase = reasonPhrase;
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status code returned by the Docker API.
|
||||
* @return the statusCode the status code
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return this.statusCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the reason phrase returned by the Docker API error.
|
||||
* @return the reasonPhrase
|
||||
*/
|
||||
public String getReasonPhrase() {
|
||||
return this.reasonPhrase;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the Errors from the body of the Docker API error, or {@code null} if the
|
||||
* error JSON could not be read.
|
||||
* @return the errors or {@code null}
|
||||
*/
|
||||
public Errors getErrors() {
|
||||
return this.errors;
|
||||
}
|
||||
|
||||
private static String buildMessage(URI uri, int statusCode, String reasonPhrase, Errors errors) {
|
||||
Assert.notNull(uri, "URI must not be null");
|
||||
StringBuilder message = new StringBuilder(
|
||||
"Docker API call to '" + uri + "' failed with status code " + statusCode);
|
||||
if (reasonPhrase != null && !reasonPhrase.isEmpty()) {
|
||||
message.append(" \"" + reasonPhrase + "\"");
|
||||
}
|
||||
if (errors != null && !errors.isEmpty()) {
|
||||
message.append(" " + errors);
|
||||
}
|
||||
return message.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.apache.http.config.Registry;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.HttpClientConnectionManager;
|
||||
import org.apache.http.conn.socket.ConnectionSocketFactory;
|
||||
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
|
||||
|
||||
/**
|
||||
* {@link HttpClientConnectionManager} for Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerHttpClientConnectionManager extends BasicHttpClientConnectionManager {
|
||||
|
||||
DockerHttpClientConnectionManager() {
|
||||
super(getRegistry(), null, null, new DockerDnsResolver());
|
||||
}
|
||||
|
||||
private static Registry<ConnectionSocketFactory> getRegistry() {
|
||||
RegistryBuilder<ConnectionSocketFactory> builder = RegistryBuilder.create();
|
||||
builder.register("docker", new DockerConnectionSocketFactory());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.apache.http.HttpHost;
|
||||
import org.apache.http.conn.SchemePortResolver;
|
||||
import org.apache.http.conn.UnsupportedSchemeException;
|
||||
import org.apache.http.util.Args;
|
||||
|
||||
/**
|
||||
* {@link SchemePortResolver} for Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerSchemePortResolver implements SchemePortResolver {
|
||||
|
||||
private static int DEFAULT_DOCKER_PORT = 2376;
|
||||
|
||||
@Override
|
||||
public int resolve(HttpHost host) throws UnsupportedSchemeException {
|
||||
Args.notNull(host, "HTTP host");
|
||||
String name = host.getSchemeName();
|
||||
if ("docker".equals(name)) {
|
||||
return DEFAULT_DOCKER_PORT;
|
||||
}
|
||||
throw new UnsupportedSchemeException(name + " protocol is not supported");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
/**
|
||||
* Errors returned from the Docker API.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Errors implements Iterable<Errors.Error> {
|
||||
|
||||
private final List<Error> errors;
|
||||
|
||||
@JsonCreator
|
||||
Errors(@JsonProperty("errors") List<Error> errors) {
|
||||
this.errors = (errors != null) ? errors : Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Errors.Error> iterator() {
|
||||
return this.errors.iterator();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sequential {@code Stream} of the errors.
|
||||
* @return a stream of the errors
|
||||
*/
|
||||
public Stream<Error> stream() {
|
||||
return this.errors.stream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return if the there are any contained errors.
|
||||
* @return if the errors are empty
|
||||
*/
|
||||
public boolean isEmpty() {
|
||||
return this.errors.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.errors.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* An individual Docker error.
|
||||
*/
|
||||
public static class Error {
|
||||
|
||||
private final String code;
|
||||
|
||||
private final String message;
|
||||
|
||||
@JsonCreator
|
||||
Error(String code, String message) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error code.
|
||||
* @return the error code
|
||||
*/
|
||||
public String getCode() {
|
||||
return this.code;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the error message.
|
||||
* @return the error message
|
||||
*/
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.code + ": " + this.message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
|
||||
/**
|
||||
* HTTP transport used by the {@link DockerApi}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
interface Http {
|
||||
|
||||
/**
|
||||
* Perform a HTTP GET operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Response get(URI uri) throws IOException;
|
||||
|
||||
/**
|
||||
* Perform a HTTP POST operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Response post(URI uri) throws IOException;
|
||||
|
||||
/**
|
||||
* Perform a HTTP POST operation.
|
||||
* @param uri the destination URI
|
||||
* @param contentType the content type to write
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException;
|
||||
|
||||
/**
|
||||
* Perform a HTTP PUT operation.
|
||||
* @param uri the destination URI
|
||||
* @param contentType the content type to write
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException;
|
||||
|
||||
/**
|
||||
* Perform a HTTP DELETE operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
Response delete(URI uri) throws IOException;
|
||||
|
||||
/**
|
||||
* An HTTP operation response.
|
||||
*/
|
||||
interface Response extends Closeable {
|
||||
|
||||
/**
|
||||
* Return the content of the response.
|
||||
* @return the reseponse content
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
InputStream getContent() throws IOException;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.entity.AbstractHttpEntity;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.client.HttpClients;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Content;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
|
||||
/**
|
||||
* {@link Http} implementation backed by a {@link HttpClient}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpClientHttp implements Http {
|
||||
|
||||
private final CloseableHttpClient client;
|
||||
|
||||
HttpClientHttp() {
|
||||
HttpClientBuilder builder = HttpClients.custom();
|
||||
builder.setConnectionManager(new DockerHttpClientConnectionManager());
|
||||
builder.setSchemePortResolver(new DockerSchemePortResolver());
|
||||
this.client = builder.build();
|
||||
}
|
||||
|
||||
HttpClientHttp(CloseableHttpClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP GET operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
@Override
|
||||
public Response get(URI uri) throws IOException {
|
||||
return execute(new HttpGet(uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP POST operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
@Override
|
||||
public Response post(URI uri) throws IOException {
|
||||
return execute(new HttpPost(uri));
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP POST operation.
|
||||
* @param uri the destination URI
|
||||
* @param contentType the content type to write
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response post(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException {
|
||||
return execute(new HttpPost(uri), contentType, writer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP PUT operation.
|
||||
* @param uri the destination URI
|
||||
* @param contentType the content type to write
|
||||
* @param writer a content writer
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response put(URI uri, String contentType, IOConsumer<OutputStream> writer) throws IOException {
|
||||
return execute(new HttpPut(uri), contentType, writer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a HTTP DELETE operation.
|
||||
* @param uri the destination URI
|
||||
* @return the operation response
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Response delete(URI uri) throws IOException {
|
||||
return execute(new HttpDelete(uri));
|
||||
}
|
||||
|
||||
private Response execute(HttpEntityEnclosingRequestBase request, String contentType,
|
||||
IOConsumer<OutputStream> writer) throws IOException {
|
||||
request.setHeader(HttpHeaders.CONTENT_TYPE, contentType);
|
||||
request.setEntity(new WritableHttpEntity(writer));
|
||||
return execute(request);
|
||||
}
|
||||
|
||||
private Response execute(HttpUriRequest request) throws IOException {
|
||||
CloseableHttpResponse response = this.client.execute(request);
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
int statusCode = statusLine.getStatusCode();
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
return new HttpClientResponse(response);
|
||||
}
|
||||
Errors errors = null;
|
||||
if (statusCode >= 400 && statusCode < 500) {
|
||||
try {
|
||||
errors = SharedObjectMapper.get().readValue(entity.getContent(), Errors.class);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
}
|
||||
}
|
||||
EntityUtils.consume(entity);
|
||||
throw new DockerException(request.getURI(), statusCode, statusLine.getReasonPhrase(), errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link HttpEntity} to send {@link Content} content.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
private class WritableHttpEntity extends AbstractHttpEntity {
|
||||
|
||||
private final IOConsumer<OutputStream> writer;
|
||||
|
||||
WritableHttpEntity(IOConsumer<OutputStream> writer) {
|
||||
this.writer = writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRepeatable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getContentLength() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getContent() throws IOException, UnsupportedOperationException {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
this.writer.accept(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStreaming() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* An HTTP operation response.
|
||||
*/
|
||||
private static class HttpClientResponse implements Response {
|
||||
|
||||
private final CloseableHttpResponse response;
|
||||
|
||||
HttpClientResponse(CloseableHttpResponse response) {
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getContent() throws IOException {
|
||||
return this.response.getEntity().getContent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.response.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
/**
|
||||
* A {@link ProgressUpdateEvent} fired as an image is loaded.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class LoadImageUpdateEvent extends ProgressUpdateEvent {
|
||||
|
||||
private final String stream;
|
||||
|
||||
@JsonCreator
|
||||
public LoadImageUpdateEvent(String stream, String status, ProgressDetail progressDetail, String progress) {
|
||||
super(status, progressDetail, progress);
|
||||
this.stream = stream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the stream response or {@code null} if no response is available.
|
||||
* @return the stream response.
|
||||
*/
|
||||
public String getStream() {
|
||||
return this.stream;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* An update event used to provide log updates.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class LogUpdateEvent extends UpdateEvent {
|
||||
|
||||
private static final Pattern ANSI_PATTERN = Pattern.compile("\u001B\\[[;\\d]*m");
|
||||
|
||||
private static final Pattern TRAILING_NEW_LINE_PATTERN = Pattern.compile("\\n$");
|
||||
|
||||
private final StreamType streamType;
|
||||
|
||||
private final byte[] payload;
|
||||
|
||||
private final String string;
|
||||
|
||||
LogUpdateEvent(StreamType streamType, byte[] payload) {
|
||||
this.streamType = streamType;
|
||||
this.payload = payload;
|
||||
String string = new String(payload, StandardCharsets.UTF_8);
|
||||
string = ANSI_PATTERN.matcher(string).replaceAll("");
|
||||
string = TRAILING_NEW_LINE_PATTERN.matcher(string).replaceAll("");
|
||||
this.string = string;
|
||||
}
|
||||
|
||||
public void print() {
|
||||
switch (this.streamType) {
|
||||
case STD_OUT:
|
||||
System.out.println(this);
|
||||
return;
|
||||
case STD_ERR:
|
||||
System.err.println(this);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public StreamType getStreamType() {
|
||||
return this.streamType;
|
||||
}
|
||||
|
||||
public byte[] getPayload() {
|
||||
return this.payload;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
static void readAll(InputStream inputStream, Consumer<LogUpdateEvent> consumer) throws IOException {
|
||||
try {
|
||||
LogUpdateEvent event;
|
||||
while ((event = LogUpdateEvent.read(inputStream)) != null) {
|
||||
consumer.accept(event);
|
||||
}
|
||||
}
|
||||
finally {
|
||||
inputStream.close();
|
||||
}
|
||||
}
|
||||
|
||||
private static LogUpdateEvent read(InputStream inputStream) throws IOException {
|
||||
byte[] header = read(inputStream, 8);
|
||||
if (header == null) {
|
||||
return null;
|
||||
}
|
||||
StreamType streamType = StreamType.values()[header[0]];
|
||||
long size = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
size = (size << 8) + (header[i + 4] & 0xff);
|
||||
}
|
||||
byte[] payload = read(inputStream, size);
|
||||
return new LogUpdateEvent(streamType, payload);
|
||||
}
|
||||
|
||||
private static byte[] read(InputStream inputStream, long size) throws IOException {
|
||||
byte[] data = new byte[(int) size];
|
||||
int offset = 0;
|
||||
do {
|
||||
int amountRead = inputStream.read(data, offset, data.length - offset);
|
||||
if (amountRead == -1) {
|
||||
return null;
|
||||
}
|
||||
offset += amountRead;
|
||||
}
|
||||
while (offset < data.length);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream types supported by the event.
|
||||
*/
|
||||
public enum StreamType {
|
||||
|
||||
/**
|
||||
* Input from {@code stdin}.
|
||||
*/
|
||||
STD_IN,
|
||||
|
||||
/**
|
||||
* Output to {@code stdout}.
|
||||
*/
|
||||
STD_OUT,
|
||||
|
||||
/**
|
||||
* Output to {@code stderr}.
|
||||
*/
|
||||
STD_ERR
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
/**
|
||||
* An {@link UpdateEvent} that includes progress information.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public abstract class ProgressUpdateEvent extends UpdateEvent {
|
||||
|
||||
private final String status;
|
||||
|
||||
private final ProgressDetail progressDetail;
|
||||
|
||||
private final String progress;
|
||||
|
||||
protected ProgressUpdateEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||
this.status = status;
|
||||
this.progressDetail = (ProgressDetail.isEmpty(progressDetail)) ? null : progressDetail;
|
||||
this.progress = progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the status for the update. For example, "Extracting" or "Downloading".
|
||||
* @return the status of the update.
|
||||
*/
|
||||
public String getStatus() {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return progress details if available.
|
||||
* @return progress details or {@code null}
|
||||
*/
|
||||
public ProgressDetail getProgressDetail() {
|
||||
return this.progressDetail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a text based progress bar if progress information is available.
|
||||
* @return the progress bar or {@code null}
|
||||
*/
|
||||
public String getProgress() {
|
||||
return this.progress;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide details about the progress of a task.
|
||||
*/
|
||||
public static class ProgressDetail {
|
||||
|
||||
private final Integer current;
|
||||
|
||||
private final Integer total;
|
||||
|
||||
@JsonCreator
|
||||
public ProgressDetail(Integer current, Integer total) {
|
||||
this.current = current;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current progress value.
|
||||
* @return the current progress
|
||||
*/
|
||||
public int getCurrent() {
|
||||
return this.current;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total progress possible value.
|
||||
* @return the total progress possible
|
||||
*/
|
||||
public int getTotal() {
|
||||
return this.total;
|
||||
}
|
||||
|
||||
public static boolean isEmpty(ProgressDetail progressDetail) {
|
||||
return progressDetail == null || progressDetail.current == null || progressDetail.total == null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
|
||||
/**
|
||||
* A {@link ProgressUpdateEvent} fired as an image is pulled.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class PullImageUpdateEvent extends ProgressUpdateEvent {
|
||||
|
||||
private final String id;
|
||||
|
||||
@JsonCreator
|
||||
public PullImageUpdateEvent(String id, String status, ProgressDetail progressDetail, String progress) {
|
||||
super(status, progressDetail, progress);
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the layer being updated if available.
|
||||
* @return the ID of the updated layer or {@code null}
|
||||
*/
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.PrintStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Utility to render a simple progress bar based on consumed {@link TotalProgressEvent}
|
||||
* objects.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class TotalProgressBar implements Consumer<TotalProgressEvent> {
|
||||
|
||||
private final char progressChar;
|
||||
|
||||
private final boolean bookend;
|
||||
|
||||
private final PrintStream out;
|
||||
|
||||
private int printed;
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressBar} instance.
|
||||
* @param prefix the prefix to output
|
||||
*/
|
||||
public TotalProgressBar(String prefix) {
|
||||
this(prefix, System.out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressBar} instance.
|
||||
* @param prefix the prefix to output
|
||||
* @param out the output print stream to use
|
||||
*/
|
||||
public TotalProgressBar(String prefix, PrintStream out) {
|
||||
this(prefix, '#', true, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressBar} instance.
|
||||
* @param prefix the prefix to output
|
||||
* @param progressChar the progress char to print
|
||||
* @param bookend if bookends should be printed
|
||||
* @param out the output print stream to use
|
||||
*/
|
||||
public TotalProgressBar(String prefix, char progressChar, boolean bookend, PrintStream out) {
|
||||
this.progressChar = progressChar;
|
||||
this.bookend = bookend;
|
||||
if (prefix != null && prefix.length() > 0) {
|
||||
out.print(prefix);
|
||||
out.print(" ");
|
||||
}
|
||||
if (bookend) {
|
||||
out.print("[ ");
|
||||
}
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(TotalProgressEvent event) {
|
||||
int percent = event.getPercent() / 2;
|
||||
while (this.printed < percent) {
|
||||
this.out.print(this.progressChar);
|
||||
this.printed++;
|
||||
}
|
||||
if (event.getPercent() == 100) {
|
||||
this.out.println(this.bookend ? " ]" : "");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Event published by the {@link TotalProgressPullListener} showing the total progress of
|
||||
* an operation.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class TotalProgressEvent {
|
||||
|
||||
private final int percent;
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressEvent} with a specific percent value.
|
||||
* @param percent the progress as a percentage
|
||||
*/
|
||||
public TotalProgressEvent(int percent) {
|
||||
Assert.isTrue(percent >= 0 && percent <= 100, "Percent must be in the range 0 to 100");
|
||||
this.percent = percent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the total progress.
|
||||
* @return the total progress
|
||||
*/
|
||||
public int getPercent() {
|
||||
return this.percent;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail;
|
||||
|
||||
/**
|
||||
* {@link UpdateListener} that calculates the total progress of the entire pull operation
|
||||
* and publishes {@link TotalProgressEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class TotalProgressPullListener implements UpdateListener<PullImageUpdateEvent> {
|
||||
|
||||
private final Map<String, Layer> layers = new ConcurrentHashMap<>();
|
||||
|
||||
private final Consumer<TotalProgressEvent> consumer;
|
||||
|
||||
private boolean progressStarted;
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressPullListener} that prints a progress bar to
|
||||
* {@link System#out}.
|
||||
* @param prefix the prefix to output
|
||||
*/
|
||||
public TotalProgressPullListener(String prefix) {
|
||||
this(new TotalProgressBar(prefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link TotalProgressPullListener} that sends {@link TotalProgressEvent
|
||||
* events} to the given consumer.
|
||||
* @param consumer the consumer that receives {@link TotalProgressEvent progress
|
||||
* events}
|
||||
*/
|
||||
public TotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
||||
this.consumer = consumer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(PullImageUpdateEvent event) {
|
||||
if (event.getId() != null) {
|
||||
this.layers.computeIfAbsent(event.getId(), Layer::new).update(event);
|
||||
}
|
||||
this.progressStarted = this.progressStarted || event.getProgress() != null;
|
||||
if (this.progressStarted) {
|
||||
publish(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFinish() {
|
||||
this.layers.values().forEach(Layer::finish);
|
||||
publish(100);
|
||||
}
|
||||
|
||||
private void publish(int fallback) {
|
||||
int count = 0;
|
||||
int total = 0;
|
||||
for (Layer layer : this.layers.values()) {
|
||||
count++;
|
||||
total += layer.getProgress();
|
||||
}
|
||||
TotalProgressEvent event = new TotalProgressEvent(
|
||||
(count != 0) ? withinPercentageBounds(total / count) : fallback);
|
||||
this.consumer.accept(event);
|
||||
}
|
||||
|
||||
private static int withinPercentageBounds(int value) {
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (value > 100) {
|
||||
return 100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress for an individual layer.
|
||||
*/
|
||||
private static class Layer {
|
||||
|
||||
private int downloadProgress;
|
||||
|
||||
private int extractProgress;
|
||||
|
||||
Layer(String id) {
|
||||
}
|
||||
|
||||
void update(PullImageUpdateEvent event) {
|
||||
if (event.getProgressDetail() != null) {
|
||||
ProgressDetail detail = event.getProgressDetail();
|
||||
if ("Downloading".equals(event.getStatus())) {
|
||||
this.downloadProgress = updateProgress(this.downloadProgress, detail);
|
||||
}
|
||||
if ("Extracting".equals(event.getStatus())) {
|
||||
this.extractProgress = updateProgress(this.extractProgress, detail);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int updateProgress(int current, ProgressDetail detail) {
|
||||
int result = withinPercentageBounds((int) ((100.0 / detail.getTotal()) * detail.getCurrent()));
|
||||
return (result > current) ? result : current;
|
||||
}
|
||||
|
||||
void finish() {
|
||||
this.downloadProgress = 100;
|
||||
this.extractProgress = 100;
|
||||
}
|
||||
|
||||
int getProgress() {
|
||||
return withinPercentageBounds((this.downloadProgress + this.extractProgress) / 2);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
/**
|
||||
* Base class for update events published by Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see UpdateListener
|
||||
*/
|
||||
public abstract class UpdateEvent {
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
/**
|
||||
* Listener for update events published from the {@link DockerApi}.
|
||||
*
|
||||
* @param <E> the update event type
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface UpdateListener<E extends UpdateEvent> {
|
||||
|
||||
/**
|
||||
* A no-op update listener.
|
||||
* @see #none()
|
||||
*/
|
||||
UpdateListener<UpdateEvent> NONE = (event) -> {
|
||||
};
|
||||
|
||||
/**
|
||||
* Called when the operation starts.
|
||||
*/
|
||||
default void onStart() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when an update event is available.
|
||||
* @param event the update event
|
||||
*/
|
||||
void onUpdate(E event);
|
||||
|
||||
/**
|
||||
* Called when the operation finishes (with or without error).
|
||||
*/
|
||||
default void onFinish() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A no-op update listener that does nothing.
|
||||
* @param <E> the event type
|
||||
* @return a no-op update listener
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
static <E extends UpdateEvent> UpdateListener<E> none() {
|
||||
return (UpdateListener<E>) NONE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A limited Docker API providing the operations needed by pack.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Configuration used when creating a new container.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class ContainerConfig {
|
||||
|
||||
private final String json;
|
||||
|
||||
ContainerConfig(String user, ImageReference image, String command, List<String> args, Map<String, String> labels,
|
||||
Map<String, String> binds) throws IOException {
|
||||
Assert.notNull(image, "Image must not be null");
|
||||
Assert.hasText(command, "Command must not be empty");
|
||||
ObjectMapper objectMapper = SharedObjectMapper.get();
|
||||
ObjectNode node = objectMapper.createObjectNode();
|
||||
if (StringUtils.hasText(user)) {
|
||||
node.put("User", user);
|
||||
}
|
||||
node.put("Image", image.toString());
|
||||
ArrayNode commandNode = node.putArray("Cmd");
|
||||
commandNode.add(command);
|
||||
args.forEach(commandNode::add);
|
||||
ObjectNode labelsNode = node.putObject("Labels");
|
||||
labels.forEach(labelsNode::put);
|
||||
ObjectNode hostConfigNode = node.putObject("HostConfig");
|
||||
ArrayNode bindsNode = hostConfigNode.putArray("Binds");
|
||||
binds.forEach((source, dest) -> bindsNode.add(source + ":" + dest));
|
||||
this.json = objectMapper.writeValueAsString(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write this container configuration to the specified {@link OutputStream}.
|
||||
* @param outputStream the output stream
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
StreamUtils.copy(this.json, StandardCharsets.UTF_8, outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link ContainerConfig} with specific settings.
|
||||
* @param imageReference the source image for the container config
|
||||
* @param update an update callback used to customize the config
|
||||
* @return a new {@link ContainerConfig} instance
|
||||
*/
|
||||
public static ContainerConfig of(ImageReference imageReference, Consumer<Update> update) {
|
||||
Assert.notNull(imageReference, "ImageReference must not be null");
|
||||
Assert.notNull(update, "Update must not be null");
|
||||
return new Update(imageReference).run(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update class used to change data when creating a container config.
|
||||
*/
|
||||
public static class Update {
|
||||
|
||||
private final ImageReference image;
|
||||
|
||||
private String user;
|
||||
|
||||
private String command;
|
||||
|
||||
private List<String> args = new ArrayList<>();
|
||||
|
||||
private Map<String, String> labels = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, String> binds = new LinkedHashMap<>();
|
||||
|
||||
Update(ImageReference image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
private ContainerConfig run(Consumer<Update> update) {
|
||||
update.accept(this);
|
||||
try {
|
||||
return new ContainerConfig(this.user, this.image, this.command, this.args, this.labels, this.binds);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with a specific user.
|
||||
* @param user the user to set
|
||||
*/
|
||||
public void withUser(String user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with a specific command.
|
||||
* @param command the command to set
|
||||
* @param args additional arguments to add
|
||||
* @see #withArgs(String...)
|
||||
*/
|
||||
public void withCommand(String command, String... args) {
|
||||
this.command = command;
|
||||
withArgs(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with additional args.
|
||||
* @param args the arguments to add
|
||||
*/
|
||||
public void withArgs(String... args) {
|
||||
this.args.addAll(Arrays.asList(args));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with an additional label.
|
||||
* @param name the label name
|
||||
* @param value the label value
|
||||
*/
|
||||
public void withLabel(String name, String value) {
|
||||
this.labels.put(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with an additional bind.
|
||||
* @param sourceVolume the source volume
|
||||
* @param dest the bind destination
|
||||
*/
|
||||
public void withBind(VolumeName sourceVolume, String dest) {
|
||||
this.binds.put(sourceVolume.toString(), dest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the container config with an additional bind.
|
||||
* @param source the bind source
|
||||
* @param dest the bind destination
|
||||
*/
|
||||
public void withBind(String source, String dest) {
|
||||
this.binds.put(source, dest);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Additional content that can be written to a created container.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface ContainerContent {
|
||||
|
||||
/**
|
||||
* Return the actual content to be added.
|
||||
* @return the content
|
||||
*/
|
||||
TarArchive getArchive();
|
||||
|
||||
/**
|
||||
* Return the destination path where the content should be added.
|
||||
* @return the destination path
|
||||
*/
|
||||
String getDestinationPath();
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link ContainerContent} instance written to the
|
||||
* root of the container.
|
||||
* @param archive the archive to add
|
||||
* @return a new {@link ContainerContent} instance
|
||||
*/
|
||||
static ContainerContent of(TarArchive archive) {
|
||||
return of(archive, "/");
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link ContainerContent} instance.
|
||||
* @param archive the archive to add
|
||||
* @param destinationPath the destination path within the container
|
||||
* @return a new {@link ContainerContent} instance
|
||||
*/
|
||||
static ContainerContent of(TarArchive archive, String destinationPath) {
|
||||
Assert.notNull(archive, "Archive must not be null");
|
||||
Assert.hasText(destinationPath, "DestinationPath must not be empty");
|
||||
return new ContainerContent() {
|
||||
|
||||
@Override
|
||||
public TarArchive getArchive() {
|
||||
return archive;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDestinationPath() {
|
||||
return destinationPath;
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A reference to a Docker container.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public final class ContainerReference {
|
||||
|
||||
private final String value;
|
||||
|
||||
private ContainerReference(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ContainerReference other = (ContainerReference) obj;
|
||||
return this.value.equals(other.value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.value.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link ContainerReference} with a specific value.
|
||||
* @param value the container reference value
|
||||
* @return a new container reference instance
|
||||
*/
|
||||
public static ContainerReference of(String value) {
|
||||
return new ContainerReference(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.MappedObject;
|
||||
|
||||
/**
|
||||
* Image details as returned from {@code Docker inspect}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Image extends MappedObject {
|
||||
|
||||
private final List<String> digests;
|
||||
|
||||
private final ImageConfig config;
|
||||
|
||||
private List<LayerId> layers;
|
||||
|
||||
private final String os;
|
||||
|
||||
Image(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.digests = getDigests(getNode().at("/RepoDigests"));
|
||||
this.config = new ImageConfig(getNode().at("/Config"));
|
||||
this.layers = extractLayers(valueAt("/RootFS/Layers", String[].class));
|
||||
this.os = valueAt("/Os", String.class);
|
||||
}
|
||||
|
||||
private List<String> getDigests(JsonNode node) {
|
||||
if (node.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
List<String> digests = new ArrayList<>();
|
||||
node.forEach((child) -> digests.add(child.asText()));
|
||||
return Collections.unmodifiableList(digests);
|
||||
}
|
||||
|
||||
private List<LayerId> extractLayers(String[] layers) {
|
||||
if (layers == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return Collections.unmodifiableList(Arrays.stream(layers).map(LayerId::of).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the digests of the image.
|
||||
* @return the image digests
|
||||
*/
|
||||
public List<String> getDigests() {
|
||||
return this.digests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return image config information.
|
||||
* @return the image config
|
||||
*/
|
||||
public ImageConfig getConfig() {
|
||||
return this.config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the layer IDs contained in the image.
|
||||
* @return the layer IDs.
|
||||
*/
|
||||
public List<LayerId> getLayers() {
|
||||
return this.layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the OS of the image.
|
||||
* @return the image OS
|
||||
*/
|
||||
public String getOs() {
|
||||
return (this.os != null) ? this.os : "linux";
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Image} instance from the specified JSON content.
|
||||
* @param content the JSON content
|
||||
* @return a new {@link Image} instace
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static Image of(InputStream content) throws IOException {
|
||||
return of(content, Image::new);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,293 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Content;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.InspectedContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Layout;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An image archive that can be loaded into Docker.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see #from(Image, IOConsumer)
|
||||
* @see <a href="https://github.com/moby/moby/blob/master/image/spec/v1.2.md">Docker Image
|
||||
* Specification</a>
|
||||
*/
|
||||
public class ImageArchive implements TarArchive {
|
||||
|
||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ISO_ZONED_DATE_TIME
|
||||
.withZone(ZoneOffset.UTC);
|
||||
|
||||
private static final IOConsumer<Update> NO_UPDDATES = (update) -> {
|
||||
};
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
private final ImageConfig imageConfig;
|
||||
|
||||
private final Instant createDate;
|
||||
|
||||
private final ImageReference tag;
|
||||
|
||||
private final String os;
|
||||
|
||||
private final List<LayerId> existingLayers;
|
||||
|
||||
private final List<Layer> newLayers;
|
||||
|
||||
ImageArchive(ObjectMapper objectMapper, ImageConfig imageConfig, Instant createDate, ImageReference tag, String os,
|
||||
List<LayerId> existingLayers, List<Layer> newLayers) {
|
||||
this.objectMapper = objectMapper;
|
||||
this.imageConfig = imageConfig;
|
||||
this.createDate = createDate;
|
||||
this.tag = tag;
|
||||
this.os = os;
|
||||
this.existingLayers = existingLayers;
|
||||
this.newLayers = newLayers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the image config for the archive.
|
||||
* @return the image config
|
||||
*/
|
||||
public ImageConfig getImageConfig() {
|
||||
return this.imageConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the create data of the archive.
|
||||
* @return the create date
|
||||
*/
|
||||
public Instant getCreateDate() {
|
||||
return this.createDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tag of the archive.
|
||||
* @return the tag
|
||||
*/
|
||||
public ImageReference getTag() {
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
TarArchive.of(this::write).writeTo(outputStream);
|
||||
}
|
||||
|
||||
private void write(Layout writer) throws IOException {
|
||||
List<LayerId> writtenLayers = writeLayers(writer);
|
||||
String config = writeConfig(writer, writtenLayers);
|
||||
writeManifest(writer, config, writtenLayers);
|
||||
}
|
||||
|
||||
private List<LayerId> writeLayers(Layout writer) throws IOException {
|
||||
List<LayerId> writtenLayers = new ArrayList<>();
|
||||
for (Layer layer : this.newLayers) {
|
||||
writtenLayers.add(writeLayer(writer, layer));
|
||||
}
|
||||
return Collections.unmodifiableList(writtenLayers);
|
||||
}
|
||||
|
||||
private LayerId writeLayer(Layout writer, Layer layer) throws IOException {
|
||||
LayerId id = layer.getId();
|
||||
writer.file("/" + id.getHash() + ".tar", Owner.ROOT, layer);
|
||||
return id;
|
||||
}
|
||||
|
||||
private String writeConfig(Layout writer, List<LayerId> writtenLayers) throws IOException {
|
||||
try {
|
||||
ObjectNode config = createConfig(writtenLayers);
|
||||
String json = this.objectMapper.writeValueAsString(config);
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
InspectedContent content = InspectedContent.of(Content.of(json), digest::update);
|
||||
String name = "/" + LayerId.ofSha256Digest(digest.digest()).getHash() + ".json";
|
||||
writer.file(name, Owner.ROOT, content);
|
||||
return name;
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectNode createConfig(List<LayerId> writtenLayers) {
|
||||
ObjectNode config = this.objectMapper.createObjectNode();
|
||||
config.set("config", this.imageConfig.getNodeCopy());
|
||||
config.set("created", config.textNode(getCreatedDate()));
|
||||
config.set("history", createHistory(writtenLayers));
|
||||
config.set("os", config.textNode(this.os));
|
||||
config.set("rootfs", createRootFs(writtenLayers));
|
||||
return config;
|
||||
}
|
||||
|
||||
private String getCreatedDate() {
|
||||
return DATE_FORMATTER.format(this.createDate);
|
||||
}
|
||||
|
||||
private JsonNode createHistory(List<LayerId> writtenLayers) {
|
||||
ArrayNode history = this.objectMapper.createArrayNode();
|
||||
int size = this.existingLayers.size() + writtenLayers.size();
|
||||
for (int i = 0; i < size; i++) {
|
||||
history.addObject();
|
||||
}
|
||||
return history;
|
||||
}
|
||||
|
||||
private JsonNode createRootFs(List<LayerId> writtenLayers) {
|
||||
ObjectNode rootFs = this.objectMapper.createObjectNode();
|
||||
ArrayNode diffIds = rootFs.putArray("diff_ids");
|
||||
this.existingLayers.stream().map(Object::toString).forEach(diffIds::add);
|
||||
writtenLayers.stream().map(Object::toString).forEach(diffIds::add);
|
||||
return rootFs;
|
||||
}
|
||||
|
||||
private void writeManifest(Layout writer, String config, List<LayerId> writtenLayers) throws IOException {
|
||||
ArrayNode manifest = createManifest(config, writtenLayers);
|
||||
String manifestJson = this.objectMapper.writeValueAsString(manifest);
|
||||
writer.file("/manifest.json", Owner.ROOT, Content.of(manifestJson));
|
||||
}
|
||||
|
||||
private ArrayNode createManifest(String config, List<LayerId> writtenLayers) {
|
||||
ArrayNode manifest = this.objectMapper.createArrayNode();
|
||||
ObjectNode entry = manifest.addObject();
|
||||
entry.set("Config", entry.textNode(config));
|
||||
entry.set("Layers", getManfiestLayers(writtenLayers));
|
||||
if (this.tag != null) {
|
||||
entry.set("RepoTags", entry.arrayNode().add(this.tag.toString()));
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
private ArrayNode getManfiestLayers(List<LayerId> writtenLayers) {
|
||||
ArrayNode layers = this.objectMapper.createArrayNode();
|
||||
for (int i = 0; i < this.existingLayers.size(); i++) {
|
||||
layers.add("");
|
||||
}
|
||||
writtenLayers.stream().map((id) -> id.getHash() + ".tar").forEach(layers::add);
|
||||
return layers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageArchive} based on an existing {@link Image}.
|
||||
* @param image the image that this archive is based on
|
||||
* @return the new image archive.
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static ImageArchive from(Image image) throws IOException {
|
||||
return from(image, NO_UPDDATES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageArchive} based on an existing {@link Image}.
|
||||
* @param image the image that this archive is based on
|
||||
* @param update consumer to apply updates
|
||||
* @return the new image archive.
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static ImageArchive from(Image image, IOConsumer<Update> update) throws IOException {
|
||||
return new Update(image).applyTo(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update class used to change data when creating an image archive.
|
||||
*/
|
||||
public static final class Update {
|
||||
|
||||
private final Image image;
|
||||
|
||||
private ImageConfig config;
|
||||
|
||||
private Instant createDate;
|
||||
|
||||
private ImageReference tag;
|
||||
|
||||
private final List<Layer> newLayers = new ArrayList<>();
|
||||
|
||||
private Update(Image image) {
|
||||
this.image = image;
|
||||
this.config = image.getConfig();
|
||||
}
|
||||
|
||||
private ImageArchive applyTo(IOConsumer<Update> update) throws IOException {
|
||||
update.accept(this);
|
||||
Instant createDate = (this.createDate != null) ? this.createDate : Instant.now();
|
||||
return new ImageArchive(SharedObjectMapper.get(), this.config, createDate, this.tag, this.image.getOs(),
|
||||
this.image.getLayers(), Collections.unmodifiableList(this.newLayers));
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply updates to the {@link ImageConfig}.
|
||||
* @param update consumer to apply updates
|
||||
*/
|
||||
public void withUpdatedConfig(Consumer<ImageConfig.Update> update) {
|
||||
this.config = this.config.copy(update);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new layer to the image archive.
|
||||
* @param layer the layer to add
|
||||
*/
|
||||
public void withNewLayer(Layer layer) {
|
||||
Assert.notNull(layer, "Layer must not be null");
|
||||
this.newLayers.add(layer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the create date for the image archive.
|
||||
* @param createDate the create date
|
||||
*/
|
||||
public void withCreateDate(Instant createDate) {
|
||||
Assert.notNull(createDate, "CreateDate must not be null");
|
||||
this.createDate = createDate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tag for the image archive.
|
||||
* @param tag the tag
|
||||
*/
|
||||
public void withTag(ImageReference tag) {
|
||||
Assert.notNull(tag, "Tag must not be null");
|
||||
this.tag = tag.inTaggedForm();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.MappedObject;
|
||||
|
||||
/**
|
||||
* Image configuration information.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class ImageConfig extends MappedObject {
|
||||
|
||||
private Map<String, String> labels;
|
||||
|
||||
private final Map<String, String> configEnv;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
ImageConfig(JsonNode node) {
|
||||
super(node, MethodHandles.lookup());
|
||||
this.labels = valueAt("/Labels", Map.class);
|
||||
this.configEnv = parseConfigEnv();
|
||||
}
|
||||
|
||||
private Map<String, String> parseConfigEnv() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
String[] entries = valueAt("/Env", String[].class);
|
||||
for (String entry : entries) {
|
||||
int i = entry.indexOf('=');
|
||||
String name = (i != -1) ? entry.substring(0, i) : entry;
|
||||
String value = (i != -1) ? entry.substring(i + 1) : null;
|
||||
env.put(name, value);
|
||||
}
|
||||
return Collections.unmodifiableMap(env);
|
||||
}
|
||||
|
||||
JsonNode getNodeCopy() {
|
||||
return super.getNode().deepCopy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the image labels.
|
||||
* @return the image labels
|
||||
*/
|
||||
public Map<String, String> getLabels() {
|
||||
return this.labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the image environment variables.
|
||||
* @return the env
|
||||
*/
|
||||
public Map<String, String> getEnv() {
|
||||
return this.configEnv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an updated copy of this image config.
|
||||
* @param update consumer to apply updates
|
||||
* @return an updated image config
|
||||
*/
|
||||
public ImageConfig copy(Consumer<Update> update) {
|
||||
return new Update(this).run(update);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Update class used to change data when creating a copy.
|
||||
*/
|
||||
public static final class Update {
|
||||
|
||||
private ObjectNode copy;
|
||||
|
||||
private Update(ImageConfig source) {
|
||||
this.copy = source.getNode().deepCopy();
|
||||
}
|
||||
|
||||
private ImageConfig run(Consumer<Update> update) {
|
||||
update.accept(this);
|
||||
return new ImageConfig(this.copy);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the image config with an additional label.
|
||||
* @param label the label name
|
||||
* @param value the label value
|
||||
*/
|
||||
public void withLabel(String label, String value) {
|
||||
JsonNode labels = this.copy.at("/Labels");
|
||||
if (labels.isMissingNode()) {
|
||||
labels = this.copy.putObject("Labels");
|
||||
}
|
||||
((ObjectNode) labels).put(label, value);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,137 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A Docker image name of the form {@literal "docker.io/library/ubuntu"}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see ImageReference
|
||||
* @see #of(String)
|
||||
*/
|
||||
public class ImageName {
|
||||
|
||||
private static final String DEFAULT_DOMAIN = "docker.io";
|
||||
|
||||
private static final String OFFICAL_REPOSITORY_NAME = "library";
|
||||
|
||||
private static final String LEGACY_DOMAIN = "index.docker.io";
|
||||
|
||||
private final String domain;
|
||||
|
||||
private final String name;
|
||||
|
||||
private final String string;
|
||||
|
||||
ImageName(String domain, String name) {
|
||||
Assert.hasText(domain, "Domain must not be empty");
|
||||
Assert.hasText(name, "Name must not be empty");
|
||||
this.domain = domain;
|
||||
this.name = name;
|
||||
this.string = domain + "/" + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the domain for this image name.
|
||||
* @return the domain
|
||||
*/
|
||||
public String getDomain() {
|
||||
return this.domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of this image.
|
||||
* @return the image name
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ImageName other = (ImageName) obj;
|
||||
boolean result = true;
|
||||
result = result && this.domain.equals(other.domain);
|
||||
result = result && this.name.equals(other.name);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.domain.hashCode();
|
||||
result = prime * result + this.name.hashCode();
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
public String toLegacyString() {
|
||||
if (DEFAULT_DOMAIN.equals(this.domain)) {
|
||||
return LEGACY_DOMAIN + "/" + this.name;
|
||||
}
|
||||
return this.string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageName} from the given value. The following value forms can
|
||||
* be used:
|
||||
* <ul>
|
||||
* <li>{@code name} (maps to {@code docker.io/library/name})</li>
|
||||
* <li>{@code domain/name}</li>
|
||||
* <li>{@code domain:port/name}</li>
|
||||
* </ul>
|
||||
* @param value the value to parse
|
||||
* @return an {@link ImageName} instance
|
||||
*/
|
||||
public static ImageName of(String value) {
|
||||
String[] split = split(value);
|
||||
return new ImageName(split[0], split[1]);
|
||||
}
|
||||
|
||||
static String[] split(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
String domain = DEFAULT_DOMAIN;
|
||||
int firstSlash = value.indexOf('/');
|
||||
if (firstSlash != -1) {
|
||||
String firstSegment = value.substring(0, firstSlash);
|
||||
if (firstSegment.contains(".") || firstSegment.contains(":") || "localhost".equals(firstSegment)) {
|
||||
domain = LEGACY_DOMAIN.equals(firstSegment) ? DEFAULT_DOMAIN : firstSegment;
|
||||
value = value.substring(firstSlash + 1);
|
||||
}
|
||||
}
|
||||
if (DEFAULT_DOMAIN.equals(domain) && !value.contains("/")) {
|
||||
value = OFFICAL_REPOSITORY_NAME + "/" + value;
|
||||
}
|
||||
return new String[] { domain, value };
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,268 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* A reference to a Docker image of the form {@code "imagename[:tag|@digest]"}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
* @see ImageName
|
||||
* @see <a href=
|
||||
* "https://stackoverflow.com/questions/37861791/how-are-docker-image-names-parsed">How
|
||||
* are Docker image names parsed?</a>
|
||||
*/
|
||||
public final class ImageReference {
|
||||
|
||||
private static final String LATEST = "latest";
|
||||
|
||||
private static final Pattern TRAILING_VERSION_PATTERN = Pattern.compile("^(.*)(\\-\\d+)$");
|
||||
|
||||
private final ImageName name;
|
||||
|
||||
private final String tag;
|
||||
|
||||
private final String digest;
|
||||
|
||||
private final String string;
|
||||
|
||||
private ImageReference(ImageName name, String tag, String digest) {
|
||||
Assert.notNull(name, "Name must not be null");
|
||||
this.name = name;
|
||||
this.tag = tag;
|
||||
this.digest = digest;
|
||||
this.string = buildString(name.toString(), tag, digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the domain for this image name.
|
||||
* @return the domain
|
||||
* @see ImageName#getDomain()
|
||||
*/
|
||||
public String getDomain() {
|
||||
return this.name.getDomain();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the name of this image.
|
||||
* @return the image name
|
||||
* @see ImageName#getName()
|
||||
*/
|
||||
public String getName() {
|
||||
return this.name.getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the tag from the reference or {@code null}.
|
||||
* @return the referenced tag
|
||||
*/
|
||||
public String getTag() {
|
||||
return this.tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the digest from the reference or {@code null}.
|
||||
* @return the referenced digest
|
||||
*/
|
||||
public String getDigest() {
|
||||
return this.digest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
ImageReference other = (ImageReference) obj;
|
||||
boolean result = true;
|
||||
result = result && this.name.equals(other.name);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.tag, other.tag);
|
||||
result = result && ObjectUtils.nullSafeEquals(this.digest, other.digest);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + this.name.hashCode();
|
||||
result = prime * result + ObjectUtils.nullSafeHashCode(this.tag);
|
||||
result = prime * result + ObjectUtils.nullSafeHashCode(this.digest);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.string;
|
||||
}
|
||||
|
||||
public String toLegacyString() {
|
||||
return buildString(this.name.toLegacyString(), this.tag, this.digest);
|
||||
}
|
||||
|
||||
private String buildString(String name, String tag, String digest) {
|
||||
StringBuilder string = new StringBuilder(name);
|
||||
if (tag != null) {
|
||||
string.append(":").append(tag);
|
||||
}
|
||||
if (digest != null) {
|
||||
string.append("@").append(digest);
|
||||
}
|
||||
return string.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} with an updated digest.
|
||||
* @param digest the new digest
|
||||
* @return an updated image reference
|
||||
*/
|
||||
public ImageReference withDigest(String digest) {
|
||||
return new ImageReference(this.name, null, digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an {@link ImageReference} in the form {@code "imagename:tag"}. If the tag
|
||||
* has not been defined then {@code latest} is used.
|
||||
* @return the image reference in tagged form
|
||||
* @throws IllegalStateException if the image reference contains a digest
|
||||
*/
|
||||
public ImageReference inTaggedForm() {
|
||||
Assert.state(this.digest == null, "Image reference '" + this + "' cannot contain a digest");
|
||||
return new ImageReference(this.name, (this.tag != null) ? this.tag : LATEST, this.digest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} instance deduced from a source JAR file that
|
||||
* follows common Java naming conventions.
|
||||
* @param jarFile the source jar file
|
||||
* @return an {@link ImageName} for the jar file.
|
||||
*/
|
||||
public static ImageReference forJarFile(File jarFile) {
|
||||
String filename = jarFile.getName();
|
||||
Assert.isTrue(filename.toLowerCase().endsWith(".jar"), "File '" + jarFile + "' is not a JAR");
|
||||
filename = filename.substring(0, filename.length() - 4);
|
||||
int firstDot = filename.indexOf('.');
|
||||
if (firstDot == -1) {
|
||||
return ImageReference.of(filename);
|
||||
}
|
||||
String name = filename.substring(0, firstDot);
|
||||
String version = filename.substring(firstDot + 1);
|
||||
Matcher matcher = TRAILING_VERSION_PATTERN.matcher(name);
|
||||
if (matcher.matches()) {
|
||||
name = matcher.group(1);
|
||||
version = matcher.group(2).substring(1) + "." + version;
|
||||
}
|
||||
return of(ImageName.of(name), version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image name with a random suffix.
|
||||
* @param prefix the name prefix
|
||||
* @return a random image reference
|
||||
*/
|
||||
public static ImageReference random(String prefix) {
|
||||
return ImageReference.random(prefix, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image name with a random suffix.
|
||||
* @param prefix the name prefix
|
||||
* @param randomLength the number of chars in the random part of the name
|
||||
* @return a random image reference
|
||||
*/
|
||||
public static ImageReference random(String prefix, int randomLength) {
|
||||
return of(RandomString.generate(prefix, randomLength));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} from the given value. The following value forms
|
||||
* can be used:
|
||||
* <ul>
|
||||
* <li>{@code name} (maps to {@code docker.io/library/name})</li>
|
||||
* <li>{@code domain/name}</li>
|
||||
* <li>{@code domain:port/name}</li>
|
||||
* <li>{@code domain:port/name:tag}</li>
|
||||
* <li>{@code domain:port/name@digest}</li>
|
||||
* </ul>
|
||||
* @param value the value to parse
|
||||
* @return an {@link ImageName} instance
|
||||
*/
|
||||
public static ImageReference of(String value) {
|
||||
Assert.hasText(value, "Value must not be null");
|
||||
String[] domainAndValue = ImageName.split(value);
|
||||
return of(domainAndValue[0], domainAndValue[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} from the given {@link ImageName}.
|
||||
* @param name the image name
|
||||
* @return a new image reference
|
||||
*/
|
||||
public static ImageReference of(ImageName name) {
|
||||
return new ImageReference(name, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} from the given {@link ImageName} and tag.
|
||||
* @param name the image name
|
||||
* @param tag the referenced tag
|
||||
* @return a new image reference
|
||||
*/
|
||||
public static ImageReference of(ImageName name, String tag) {
|
||||
return new ImageReference(name, tag, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link ImageReference} from the given {@link ImageName}, tag and
|
||||
* digest.
|
||||
* @param name the image name
|
||||
* @param tag the referenced tag
|
||||
* @param digest the referenced digest
|
||||
* @return a new image reference
|
||||
*/
|
||||
public static ImageReference of(ImageName name, String tag, String digest) {
|
||||
return new ImageReference(name, tag, digest);
|
||||
}
|
||||
|
||||
private static ImageReference of(String domain, String value) {
|
||||
String digest = null;
|
||||
int lastAt = value.indexOf('@');
|
||||
if (lastAt != -1) {
|
||||
digest = value.substring(lastAt + 1);
|
||||
value = value.substring(0, lastAt);
|
||||
}
|
||||
String tag = null;
|
||||
int firstColon = value.indexOf(':');
|
||||
if (firstColon != -1) {
|
||||
tag = value.substring(firstColon + 1);
|
||||
value = value.substring(0, firstColon);
|
||||
}
|
||||
ImageName name = new ImageName(domain, value);
|
||||
return new ImageReference(name, tag, digest);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,94 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Content;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.InspectedContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Layout;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A layer that can be written to an {@link ImageArchive}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Layer implements Content {
|
||||
|
||||
private final Content content;
|
||||
|
||||
private final LayerId id;
|
||||
|
||||
Layer(TarArchive tarArchive) throws NoSuchAlgorithmException, IOException {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
this.content = InspectedContent.of(tarArchive::writeTo, digest::update);
|
||||
this.id = LayerId.ofSha256Digest(digest.digest());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the ID of the layer.
|
||||
* @return the layer ID
|
||||
*/
|
||||
public LayerId getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.content.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
this.content.writeTo(outputStream);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link Layer} with a specific {@link Layout}.
|
||||
* @param layout the layer layout
|
||||
* @return a new layer instance
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static Layer of(IOConsumer<Layout> layout) throws IOException {
|
||||
Assert.notNull(layout, "Layout must not be null");
|
||||
return fromTarArchive(TarArchive.of(layout));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link Layer} from a {@link TarArchive}.
|
||||
* @param tarArchive the contents of the layer
|
||||
* @return a new layer instance
|
||||
* @throws IOException on error
|
||||
*/
|
||||
public static Layer fromTarArchive(TarArchive tarArchive) throws IOException {
|
||||
Assert.notNull(tarArchive, "TarArchive must not be null");
|
||||
try {
|
||||
return new Layer(tarArchive);
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A layer ID as used inside a Docker image of the form {@code algorithm: hash}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public final class LayerId {
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String algorithm;
|
||||
|
||||
private final String hash;
|
||||
|
||||
private LayerId(String value, String algorithm, String hash) {
|
||||
this.value = value;
|
||||
this.algorithm = algorithm;
|
||||
this.hash = hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the algorithm of layer.
|
||||
* @return the algorithm
|
||||
*/
|
||||
public String getAlgorithm() {
|
||||
return this.algorithm;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the hash of the layer.
|
||||
* @return the layer hash
|
||||
*/
|
||||
public String getHash() {
|
||||
return this.hash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return this.value.equals(((LayerId) obj).value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.value.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LayerId} with the specified value.
|
||||
* @param value the layer ID value of the form {@code algorithm: hash}
|
||||
* @return a new layer ID instance
|
||||
*/
|
||||
public static LayerId of(String value) {
|
||||
Assert.hasText(value, "Value must not be empty");
|
||||
int i = value.indexOf(':');
|
||||
Assert.isTrue(i >= 0, "Invalid layer ID '" + value + "'");
|
||||
return new LayerId(value, value.substring(0, i), value.substring(i + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link LayerId} from a SHA-256 digest.
|
||||
* @param digest the digest
|
||||
* @return a new layer ID instance
|
||||
*/
|
||||
public static LayerId ofSha256Digest(byte[] digest) {
|
||||
Assert.notNull(digest, "Digest must not be null");
|
||||
Assert.isTrue(digest.length == 32, "Digest must be exactly 32 bytes");
|
||||
String algorithm = "sha256";
|
||||
String hash = String.format("%32x", new BigInteger(1, digest));
|
||||
return new LayerId(algorithm + ":" + hash, algorithm, hash);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.util.Random;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Utility class used to generate random strings.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
final class RandomString {
|
||||
|
||||
private static final Random random = new Random();
|
||||
|
||||
private RandomString() {
|
||||
}
|
||||
|
||||
static String generate(String prefix, int randomLength) {
|
||||
Assert.notNull(prefix, "Prefix must not be null");
|
||||
return prefix + generateRandom(randomLength);
|
||||
}
|
||||
|
||||
static CharSequence generateRandom(int length) {
|
||||
IntStream chars = random.ints('a', 'z' + 1).limit(length);
|
||||
return chars.collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A Docker volume name.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public final class VolumeName {
|
||||
|
||||
private final String value;
|
||||
|
||||
private VolumeName(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj) {
|
||||
return true;
|
||||
}
|
||||
if (obj == null || getClass() != obj.getClass()) {
|
||||
return false;
|
||||
}
|
||||
return this.value.equals(((VolumeName) obj).value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.value.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link VolumeName} with a random name.
|
||||
* @param prefix the prefix to use with the random name
|
||||
* @return a randomly named volume
|
||||
*/
|
||||
public static VolumeName random(String prefix) {
|
||||
return random(prefix, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link VolumeName} with a random name.
|
||||
* @param prefix the prefix to use with the random name
|
||||
* @param randomLength the number of chars in the random part of the name
|
||||
* @return a randomly volume reference
|
||||
*/
|
||||
public static VolumeName random(String prefix, int randomLength) {
|
||||
return of(RandomString.generate(prefix, randomLength));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link VolumeName} based on an object. The resulting
|
||||
* name will be based off a SHA-256 digest of the given object's {@code toString()}
|
||||
* method.
|
||||
* @param <S> the source object type
|
||||
* @param source the source object
|
||||
* @param prefix the prefix to use with the volume name
|
||||
* @param suffix the suffix to use with the volume name
|
||||
* @param digestLength the number of chars in the digest part of the name
|
||||
* @return a name based off the image reference
|
||||
*/
|
||||
public static <S> VolumeName basedOn(S source, String prefix, String suffix, int digestLength) {
|
||||
return basedOn(source, Object::toString, prefix, suffix, digestLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link VolumeName} based on an object. The resulting
|
||||
* name will be based off a SHA-256 digest of the given object's name.
|
||||
* @param <S> the source object type
|
||||
* @param source the source object
|
||||
* @param nameExtractor a method to extract the name of the object
|
||||
* @param prefix the prefix to use with the volume name
|
||||
* @param suffix the suffix to use with the volume name
|
||||
* @param digestLength the number of chars in the digest part of the name
|
||||
* @return a name based off the image reference
|
||||
*/
|
||||
public static <S> VolumeName basedOn(S source, Function<S, String> nameExtractor, String prefix, String suffix,
|
||||
int digestLength) {
|
||||
Assert.notNull(source, "Source must not be null");
|
||||
Assert.notNull(nameExtractor, "NameExtractor must not be null");
|
||||
Assert.notNull(prefix, "Prefix must not be null");
|
||||
Assert.notNull(suffix, "Suffix must not be null");
|
||||
return of(prefix + getDigest(nameExtractor.apply(source), digestLength) + suffix);
|
||||
}
|
||||
|
||||
private static String getDigest(String name, int length) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance("sha-256");
|
||||
return asHexString(digest.digest(name.getBytes(StandardCharsets.UTF_8)), length);
|
||||
}
|
||||
catch (NoSuchAlgorithmException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static String asHexString(byte[] digest, int digestLength) {
|
||||
Assert.isTrue(digestLength <= digest.length, "DigestLength must be less than or equal to " + digest.length);
|
||||
byte[] shortDigest = new byte[6];
|
||||
System.arraycopy(digest, 0, shortDigest, 0, shortDigest.length);
|
||||
return String.format("%0" + digestLength + "x", new BigInteger(1, shortDigest));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link VolumeName} with a specific value.
|
||||
* @param value the volme reference value
|
||||
* @return a new {@link VolumeName} instance
|
||||
*/
|
||||
public static VolumeName of(String value) {
|
||||
Assert.notNull(value, "Value must not be null");
|
||||
return new VolumeName(value);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Docker types.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* Content with a known size that can be written to an {@link OutputStream}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface Content {
|
||||
|
||||
/**
|
||||
* The size of the content in bytes.
|
||||
* @return the content size
|
||||
*/
|
||||
int size();
|
||||
|
||||
/**
|
||||
* Write the content to the given output stream.
|
||||
* @param outputStream the output stream to write to
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void writeTo(OutputStream outputStream) throws IOException;
|
||||
|
||||
/**
|
||||
* Create a new {@link Content} from the given UTF-8 string.
|
||||
* @param string the string to write
|
||||
* @return a new {@link Content} instance
|
||||
*/
|
||||
static Content of(String string) {
|
||||
Assert.notNull(string, "String must not be null");
|
||||
return of(string.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Content} from the given input stream.
|
||||
* @param bytes the bytes to write
|
||||
* @return a new {@link Content} instance
|
||||
*/
|
||||
static Content of(byte[] bytes) {
|
||||
Assert.notNull(bytes, "Bytes must not be null");
|
||||
return of(bytes.length, () -> new ByteArrayInputStream(bytes));
|
||||
}
|
||||
|
||||
static Content of(File file) {
|
||||
Assert.notNull(file, "File must not be null");
|
||||
return of((int) file.length(), () -> new FileInputStream(file));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Content} from the given input stream. The stream will be closed
|
||||
* after it has been written.
|
||||
* @param size the size of the supplied input stream
|
||||
* @param supplier the input stream supplier
|
||||
* @return a new {@link Content} instance
|
||||
*/
|
||||
static Content of(int size, IOSupplier<InputStream> supplier) {
|
||||
Assert.isTrue(size >= 0, "Size must not be negative");
|
||||
Assert.notNull(supplier, "Supplier must not be null");
|
||||
return new Content() {
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
FileCopyUtils.copy(supplier.get(), outputStream);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
/**
|
||||
* Default {@link Owner} implementation.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see Owner#of(long, long)
|
||||
*/
|
||||
class DefaultOwner implements Owner {
|
||||
|
||||
private final long uid;
|
||||
|
||||
private final long gid;
|
||||
|
||||
DefaultOwner(long uid, long gid) {
|
||||
this.uid = uid;
|
||||
this.gid = gid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getUid() {
|
||||
return this.uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getGid() {
|
||||
return this.gid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.uid + "/" + this.gid;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Consumer that can safely throw {@link IOException IO exceptions}.
|
||||
*
|
||||
* @param <T> the consumed type
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface IOConsumer<T> {
|
||||
|
||||
/**
|
||||
* Performs this operation on the given argument.
|
||||
* @param t the instance to consume
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void accept(T t) throws IOException;
|
||||
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Supplier that can safely throw {@link IOException IO exceptions}.
|
||||
*
|
||||
* @param <T> the supplied type
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface IOSupplier<T> {
|
||||
|
||||
/**
|
||||
* Gets the supplied value.
|
||||
* @return the supplied value
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
T get() throws IOException;
|
||||
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* {@link Content} that is reads and inspects a source of data only once but allows it to
|
||||
* be consumed multiple times.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class InspectedContent implements Content {
|
||||
|
||||
static final int MEMORY_LIMIT = 4 * 1024 + 3;
|
||||
|
||||
private final int size;
|
||||
|
||||
private final Object content;
|
||||
|
||||
InspectedContent(int size, Object content) {
|
||||
this.size = size;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
if (this.content instanceof byte[]) {
|
||||
FileCopyUtils.copy((byte[]) this.content, outputStream);
|
||||
}
|
||||
else if (this.content instanceof File) {
|
||||
FileCopyUtils.copy(new FileInputStream((File) this.content), outputStream);
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException("Unknown content type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link InspectedContent} instance from a source input
|
||||
* stream.
|
||||
* @param inputStream the content input stream
|
||||
* @param inspectors any inspectors to apply
|
||||
* @return a new inspected content instance
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static InspectedContent of(InputStream inputStream, Inspector... inspectors) throws IOException {
|
||||
Assert.notNull(inputStream, "InputStream must not be null");
|
||||
return of((outputStream) -> FileCopyUtils.copy(inputStream, outputStream), inspectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link InspectedContent} instance from source content.
|
||||
* @param content the content
|
||||
* @param inspectors any inspectors to apply
|
||||
* @return a new inspected content instance
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static InspectedContent of(Content content, Inspector... inspectors) throws IOException {
|
||||
Assert.notNull(content, "Content must not be null");
|
||||
return of(content::writeTo, inspectors);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create an {@link InspectedContent} instance from a source write
|
||||
* method.
|
||||
* @param writer a consumer representing the write method
|
||||
* @param inspectors any inspectors to apply
|
||||
* @return a new inspected content instance
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public static InspectedContent of(IOConsumer<OutputStream> writer, Inspector... inspectors) throws IOException {
|
||||
Assert.notNull(writer, "Writer must not be null");
|
||||
InspectingOutputStream outputStream = new InspectingOutputStream(inspectors);
|
||||
try {
|
||||
writer.accept(outputStream);
|
||||
}
|
||||
finally {
|
||||
outputStream.close();
|
||||
}
|
||||
return new InspectedContent(outputStream.getSize(), outputStream.getContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface that can be used to inspect content as it is initially read.
|
||||
*/
|
||||
public interface Inspector {
|
||||
|
||||
/**
|
||||
* Update inspected information based on the provided bytes.
|
||||
* @param input the array of bytes.
|
||||
* @param offset the offset to start from in the array of bytes.
|
||||
* @param len the number of bytes to use, starting at {@code offset}.
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void update(byte[] input, int offset, int len) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal {@link OutputStream} used to capture the content either as bytes, or to a
|
||||
* File if the content is too large.
|
||||
*/
|
||||
private static final class InspectingOutputStream extends OutputStream {
|
||||
|
||||
private final Inspector[] inspectors;
|
||||
|
||||
private int size;
|
||||
|
||||
private OutputStream delegate;
|
||||
|
||||
private File tempFile;
|
||||
|
||||
private byte[] singleByteBuffer = new byte[0];
|
||||
|
||||
private InspectingOutputStream(Inspector[] inspectors) {
|
||||
this.inspectors = inspectors;
|
||||
this.delegate = new ByteArrayOutputStream();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
this.singleByteBuffer[0] = (byte) (b & 0xFF);
|
||||
write(this.singleByteBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
int size = len - off;
|
||||
if (this.tempFile == null && (this.size + size) > MEMORY_LIMIT) {
|
||||
convertToTempFile();
|
||||
}
|
||||
this.delegate.write(b, off, len);
|
||||
for (Inspector inspector : this.inspectors) {
|
||||
inspector.update(b, off, len);
|
||||
}
|
||||
this.size += size;
|
||||
}
|
||||
|
||||
private void convertToTempFile() throws IOException {
|
||||
this.tempFile = File.createTempFile("buildpack", ".tmp");
|
||||
byte[] bytes = ((ByteArrayOutputStream) this.delegate).toByteArray();
|
||||
this.delegate = new FileOutputStream(this.tempFile);
|
||||
StreamUtils.copy(bytes, this.delegate);
|
||||
}
|
||||
|
||||
private Object getContent() {
|
||||
return (this.tempFile != null) ? this.tempFile : ((ByteArrayOutputStream) this.delegate).toByteArray();
|
||||
}
|
||||
|
||||
private int getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Interface that can be used to write a file/folder layout.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface Layout {
|
||||
|
||||
/**
|
||||
* Add a folder to the content.
|
||||
* @param name the full name of the folder to add.
|
||||
* @param owner the owner of the folder
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void folder(String name, Owner owner) throws IOException;
|
||||
|
||||
/**
|
||||
* Write a file to the content.
|
||||
* @param name the full name of the file to add.
|
||||
* @param owner the owner of the folder
|
||||
* @param content the content to add
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void file(String name, Owner owner, Content content) throws IOException;
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
/**
|
||||
* A user and group ID that can be used to indicate file ownership.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public interface Owner {
|
||||
|
||||
/**
|
||||
* Owner for root ownership.
|
||||
*/
|
||||
Owner ROOT = Owner.of(0, 0);
|
||||
|
||||
/**
|
||||
* Return the user identifier (UID) of the owner.
|
||||
* @return the user identifier
|
||||
*/
|
||||
long getUid();
|
||||
|
||||
/**
|
||||
* Return the group identifier (GID) of the owner.
|
||||
* @return the group identifier
|
||||
*/
|
||||
long getGid();
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link Owner} with specified user/group identifier.
|
||||
* @param uid the user identifier
|
||||
* @param gid the group identifier
|
||||
* @return a new {@link Owner} instance
|
||||
*/
|
||||
static Owner of(long uid, long gid) {
|
||||
return new DefaultOwner(uid, gid);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.time.Instant;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
/**
|
||||
* A TAR archive that can be written to an output stream.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface TarArchive {
|
||||
|
||||
/**
|
||||
* {@link Instant} that can be used to normalize TAR files so all entries have the
|
||||
* same modification time.
|
||||
*/
|
||||
Instant NORMALIZED_TIME = OffsetDateTime.of(1980, 1, 1, 0, 0, 1, 0, ZoneOffset.UTC).toInstant();
|
||||
|
||||
/**
|
||||
* Write the TAR archive to the given output stream.
|
||||
* @param outputStream the output stream to write to
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
void writeTo(OutputStream outputStream) throws IOException;
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link TarArchive} instance with a specific layout.
|
||||
* @param layout the TAR layout
|
||||
* @return a new {@link TarArchive} instance
|
||||
*/
|
||||
static TarArchive of(IOConsumer<Layout> layout) {
|
||||
return (outputStream) -> {
|
||||
TarLayoutWriter writer = new TarLayoutWriter(outputStream);
|
||||
layout.accept(writer);
|
||||
writer.finish();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to adapt a ZIP file to {@link TarArchive}.
|
||||
* @param zip the source zip file
|
||||
* @param owner the owner of the entries in the TAR
|
||||
* @return a new {@link TarArchive} instance
|
||||
*/
|
||||
static TarArchive fromZip(File zip, Owner owner) {
|
||||
return new ZipFileTarArchive(zip, owner);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,84 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.tar.TarConstants;
|
||||
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* {@link Layout} for writing TAR archive content directly to an {@link OutputStream}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TarLayoutWriter implements Layout, Closeable {
|
||||
|
||||
static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli();
|
||||
|
||||
private TarArchiveOutputStream outputStream;
|
||||
|
||||
TarLayoutWriter(OutputStream outputStream) {
|
||||
this.outputStream = new TarArchiveOutputStream(outputStream);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void folder(String name, Owner owner) throws IOException {
|
||||
this.outputStream.putArchiveEntry(createFolderEntry(name, owner));
|
||||
this.outputStream.closeArchiveEntry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void file(String name, Owner owner, Content content) throws IOException {
|
||||
this.outputStream.putArchiveEntry(createFileEntry(name, owner, content.size()));
|
||||
content.writeTo(StreamUtils.nonClosing(this.outputStream));
|
||||
this.outputStream.closeArchiveEntry();
|
||||
}
|
||||
|
||||
private TarArchiveEntry createFolderEntry(String name, Owner owner) {
|
||||
return createEntry(name, owner, TarConstants.LF_DIR, 0755, 0);
|
||||
}
|
||||
|
||||
private TarArchiveEntry createFileEntry(String name, Owner owner, int size) {
|
||||
return createEntry(name, owner, TarConstants.LF_NORMAL, 0644, size);
|
||||
}
|
||||
|
||||
private TarArchiveEntry createEntry(String name, Owner owner, byte linkFlag, int mode, int size) {
|
||||
TarArchiveEntry entry = new TarArchiveEntry(name, linkFlag, true);
|
||||
entry.setUserId(owner.getUid());
|
||||
entry.setGroupId(owner.getGid());
|
||||
entry.setMode(mode);
|
||||
entry.setModTime(NORMALIZED_MOD_TIME);
|
||||
entry.setSize(size);
|
||||
return entry;
|
||||
}
|
||||
|
||||
void finish() throws IOException {
|
||||
this.outputStream.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.outputStream.close();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.Enumeration;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
|
||||
import org.apache.commons.compress.archivers.tar.TarConstants;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipFile;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* Adapter class to convert a ZIP file to a {@link TarArchive}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ZipFileTarArchive implements TarArchive {
|
||||
|
||||
static final long NORMALIZED_MOD_TIME = TarArchive.NORMALIZED_TIME.toEpochMilli();
|
||||
|
||||
private final File zip;
|
||||
|
||||
private final Owner owner;
|
||||
|
||||
ZipFileTarArchive(File zip, Owner owner) {
|
||||
Assert.notNull(zip, "Zip must not be null");
|
||||
Assert.notNull(owner, "Owner must not be null");
|
||||
this.zip = zip;
|
||||
this.owner = owner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(OutputStream outputStream) throws IOException {
|
||||
TarArchiveOutputStream tar = new TarArchiveOutputStream(outputStream);
|
||||
try (ZipFile zipFile = new ZipFile(this.zip)) {
|
||||
Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipArchiveEntry zipEntry = entries.nextElement();
|
||||
copy(zipEntry, zipFile.getInputStream(zipEntry), tar);
|
||||
}
|
||||
}
|
||||
tar.finish();
|
||||
}
|
||||
|
||||
private void copy(ZipArchiveEntry zipEntry, InputStream zip, TarArchiveOutputStream tar) throws IOException {
|
||||
TarArchiveEntry tarEntry = convert(zipEntry);
|
||||
tar.putArchiveEntry(tarEntry);
|
||||
if (tarEntry.isFile()) {
|
||||
StreamUtils.copyRange(zip, tar, 0, tarEntry.getSize());
|
||||
}
|
||||
tar.closeArchiveEntry();
|
||||
}
|
||||
|
||||
private TarArchiveEntry convert(ZipArchiveEntry zipEntry) {
|
||||
byte linkFlag = (zipEntry.isDirectory()) ? TarConstants.LF_DIR : TarConstants.LF_NORMAL;
|
||||
TarArchiveEntry tarEntry = new TarArchiveEntry(zipEntry.getName(), linkFlag, true);
|
||||
tarEntry.setUserId(this.owner.getUid());
|
||||
tarEntry.setGroupId(this.owner.getGid());
|
||||
tarEntry.setModTime(NORMALIZED_MOD_TIME);
|
||||
if (!zipEntry.isDirectory()) {
|
||||
tarEntry.setSize(zipEntry.getSize());
|
||||
}
|
||||
return tarEntry;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* IO classes and utilities.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.io;
|
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
|
||||
/**
|
||||
* Utility class that allows JSON to be parsed and processed as it's received.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class JsonStream {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* Create a new {@link JsonStream} backed by the given object mapper.
|
||||
* @param objectMapper the object mapper to use
|
||||
*/
|
||||
public JsonStream(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream {@link ObjectNode object nodes} from the content as they become available.
|
||||
* @param content the source content
|
||||
* @param consumer the {@link ObjectNode} consumer
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public void get(InputStream content, Consumer<ObjectNode> consumer) throws IOException {
|
||||
get(content, ObjectNode.class, consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream objects from the content as they become available.
|
||||
* @param <T> the object type
|
||||
* @param content the source content
|
||||
* @param type the object type
|
||||
* @param consumer the {@link ObjectNode} consumer
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
public <T> void get(InputStream content, Class<T> type, Consumer<T> consumer) throws IOException {
|
||||
JsonFactory jsonFactory = this.objectMapper.getFactory();
|
||||
JsonParser parser = jsonFactory.createParser(content);
|
||||
while (!parser.isClosed()) {
|
||||
JsonToken token = parser.nextToken();
|
||||
if (token != null && token != JsonToken.END_OBJECT) {
|
||||
T node = read(parser, type);
|
||||
if (node != null) {
|
||||
consumer.accept(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T read(JsonParser parser, Class<T> type) throws IOException {
|
||||
if (ObjectNode.class.isAssignableFrom(type)) {
|
||||
ObjectNode node = this.objectMapper.readTree(parser);
|
||||
if (node == null || node.isMissingNode() || node.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return (T) node;
|
||||
}
|
||||
return this.objectMapper.readValue(parser, type);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.invoke.MethodHandle;
|
||||
import java.lang.invoke.MethodHandles.Lookup;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Base class for mapped JSON objects.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class MappedObject {
|
||||
|
||||
private final JsonNode node;
|
||||
|
||||
private final Lookup lookup;
|
||||
|
||||
/**
|
||||
* Create a new {@link MappedObject} instance.
|
||||
* @param node the source node
|
||||
* @param lookup method handle lookup
|
||||
*/
|
||||
protected MappedObject(JsonNode node, Lookup lookup) {
|
||||
this.node = node;
|
||||
this.lookup = lookup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the source node of the mapped object.
|
||||
* @return the source node
|
||||
*/
|
||||
protected final JsonNode getNode() {
|
||||
return this.node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the value at the given JSON path expression as a specific type.
|
||||
* @param <T> the data type
|
||||
* @param expression the JSON path expression
|
||||
* @param type the desired type. May be a simple JSON type or an interface
|
||||
* @return the value
|
||||
*/
|
||||
protected <T> T valueAt(String expression, Class<T> type) {
|
||||
return valueAt(this, this.node, this.lookup, expression, type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
protected static <T extends MappedObject> T getRoot(Object proxy) {
|
||||
MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy);
|
||||
return (T) handler.root;
|
||||
}
|
||||
|
||||
protected static <T> T valueAt(Object proxy, String expression, Class<T> type) {
|
||||
MappedInvocationHandler handler = (MappedInvocationHandler) Proxy.getInvocationHandler(proxy);
|
||||
return valueAt(handler.root, handler.node, handler.lookup, expression, type);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T valueAt(MappedObject root, JsonNode node, Lookup lookup, String expression, Class<T> type) {
|
||||
JsonNode result = node.at(expression);
|
||||
if (result.isMissingNode() && expression.startsWith("/") && expression.length() > 1
|
||||
&& Character.isLowerCase(expression.charAt(1))) {
|
||||
StringBuilder alternative = new StringBuilder(expression);
|
||||
alternative.setCharAt(1, Character.toUpperCase(alternative.charAt(1)));
|
||||
result = node.at(alternative.toString());
|
||||
}
|
||||
if (type.isInterface() && !type.getName().startsWith("java")) {
|
||||
return (T) Proxy.newProxyInstance(MappedObject.class.getClassLoader(), new Class<?>[] { type },
|
||||
new MappedInvocationHandler(root, result, lookup));
|
||||
}
|
||||
if (result.isMissingNode()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return SharedObjectMapper.get().treeToValue(result, type);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link MappedObject} instance.
|
||||
* @param <T> the mapped object type
|
||||
* @param content the JSON content for the object
|
||||
* @param factory a factory to create the mapped object from a {@link JsonNode}
|
||||
* @return the mapped object
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
protected static <T extends MappedObject> T of(String content, Function<JsonNode, T> factory) throws IOException {
|
||||
return of(content, ObjectMapper::readTree, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link MappedObject} instance.
|
||||
* @param <T> the mapped object type
|
||||
* @param content the JSON content for the object
|
||||
* @param factory a factory to create the mapped object from a {@link JsonNode}
|
||||
* @return the mapped object
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
protected static <T extends MappedObject> T of(InputStream content, Function<JsonNode, T> factory)
|
||||
throws IOException {
|
||||
return of(content, ObjectMapper::readTree, factory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a new {@link MappedObject} instance.
|
||||
* @param <T> the mapped object type
|
||||
* @param <C> the content type
|
||||
* @param content the JSON content for the object
|
||||
* @param reader the content reader
|
||||
* @param factory a factory to create the mapped object from a {@link JsonNode}
|
||||
* @return the mapped object
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
protected static <T extends MappedObject, C> T of(C content, ContentReader<C> reader, Function<JsonNode, T> factory)
|
||||
throws IOException {
|
||||
ObjectMapper objectMapper = SharedObjectMapper.get();
|
||||
JsonNode node = reader.read(objectMapper, content);
|
||||
return factory.apply(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strategy used to read JSON content.
|
||||
*
|
||||
* @param <C> the content type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
protected interface ContentReader<C> {
|
||||
|
||||
/**
|
||||
* Read JSON content as a {@link JsonNode}.
|
||||
* @param objectMapper the source object mapper
|
||||
* @param content the content to read
|
||||
* @return a {@link JsonNode}
|
||||
* @throws IOException on IO error
|
||||
*/
|
||||
JsonNode read(ObjectMapper objectMapper, C content) throws IOException;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InvocationHandler} used to support
|
||||
* {@link MappedObject#valueAt(String, Class) valueAt} with {@code interface} types.
|
||||
*/
|
||||
private static class MappedInvocationHandler implements InvocationHandler {
|
||||
|
||||
private static final String GET = "get";
|
||||
|
||||
private static final String IS = "is";
|
||||
|
||||
private final MappedObject root;
|
||||
|
||||
private final JsonNode node;
|
||||
|
||||
private final Lookup lookup;
|
||||
|
||||
MappedInvocationHandler(MappedObject root, JsonNode node, Lookup lookup) {
|
||||
this.root = root;
|
||||
this.node = node;
|
||||
this.lookup = lookup;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
Class<?> declaringClass = method.getDeclaringClass();
|
||||
if (method.isDefault()) {
|
||||
Lookup lookup = this.lookup.in(declaringClass);
|
||||
MethodHandle methodHandle = lookup.unreflectSpecial(method, declaringClass).bindTo(proxy);
|
||||
return methodHandle.invokeWithArguments();
|
||||
}
|
||||
if (declaringClass == Object.class) {
|
||||
method.invoke(proxy, args);
|
||||
}
|
||||
Assert.state(args == null || args.length == 0, "Unsupported method " + method);
|
||||
String name = getName(method.getName());
|
||||
Class<?> type = method.getReturnType();
|
||||
return valueForProperty(name, type);
|
||||
}
|
||||
|
||||
private String getName(String name) {
|
||||
StringBuilder result = new StringBuilder(name);
|
||||
if (name.startsWith(GET)) {
|
||||
result = new StringBuilder(name.substring(GET.length()));
|
||||
}
|
||||
if (name.startsWith(IS)) {
|
||||
result = new StringBuilder(name.substring(IS.length()));
|
||||
}
|
||||
Assert.state(result.length() >= 0, "Missing name");
|
||||
result.setCharAt(0, Character.toLowerCase(result.charAt(0)));
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private Object valueForProperty(String name, Class<?> type) {
|
||||
return valueAt(this.root, this.node, this.lookup, "/" + name, type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.json;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
|
||||
|
||||
/**
|
||||
* Provides access to a shared pre-configured {@link ObjectMapper}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public final class SharedObjectMapper {
|
||||
|
||||
private static final ObjectMapper INSTANCE;
|
||||
|
||||
static {
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new ParameterNamesModule());
|
||||
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CAMEL_CASE);
|
||||
INSTANCE = objectMapper;
|
||||
}
|
||||
|
||||
private SharedObjectMapper() {
|
||||
}
|
||||
|
||||
public static ObjectMapper get() {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Utilities and classes for JSON processing.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.json;
|
@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.SocketAddress;
|
||||
|
||||
/**
|
||||
* Abstract base class for custom socket implementation.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class AbstractSocket extends Socket {
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress endpoint) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect(SocketAddress endpoint, int timeout) throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isConnected() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBound() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownInput() throws IOException {
|
||||
throw new UnsupportedSocketOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdownOutput() throws IOException {
|
||||
throw new UnsupportedSocketOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getInetAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InetAddress getLocalAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getLocalSocketAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SocketAddress getRemoteSocketAddress() {
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class UnsupportedSocketOperationException extends UnsupportedOperationException {
|
||||
|
||||
UnsupportedSocketOperationException() {
|
||||
super("Unsupported socket operation");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Platform;
|
||||
import com.sun.jna.Structure;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link DomainSocket} implementation for BSD based platforms.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BsdDomainSocket extends DomainSocket {
|
||||
|
||||
private static final int MAX_PATH_LENGTH = 104;
|
||||
|
||||
static {
|
||||
Native.register(Platform.C_LIBRARY_NAME);
|
||||
}
|
||||
|
||||
BsdDomainSocket(String path) throws IOException {
|
||||
super(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void connect(String path, int handle) {
|
||||
SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8));
|
||||
connect(handle, address, address.size());
|
||||
}
|
||||
|
||||
private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException;
|
||||
|
||||
/**
|
||||
* Native {@code sockaddr_un} structure as defined in {@code sys/un.h}.
|
||||
*/
|
||||
public static class SockaddrUn extends Structure implements Structure.ByReference {
|
||||
|
||||
public byte sunLen;
|
||||
|
||||
public byte sunFamily;
|
||||
|
||||
public byte[] sunPath = new byte[MAX_PATH_LENGTH];
|
||||
|
||||
private SockaddrUn(byte sunFamily, byte[] path) {
|
||||
Assert.isTrue(path.length < MAX_PATH_LENGTH, "Path cannot exceed " + MAX_PATH_LENGTH + " bytes");
|
||||
System.arraycopy(path, 0, this.sunPath, 0, path.length);
|
||||
this.sunPath[path.length] = 0;
|
||||
this.sunLen = (byte) (fieldOffset("sunPath") + path.length);
|
||||
this.sunFamily = sunFamily;
|
||||
allocateMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.Socket;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.Native;
|
||||
import com.sun.jna.Platform;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.socket.FileDescriptor.Handle;
|
||||
|
||||
/**
|
||||
* A {@link Socket} implementation for Linux of BSD domain sockets.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public abstract class DomainSocket extends AbstractSocket {
|
||||
|
||||
private static final int SHUT_RD = 0;
|
||||
|
||||
private static final int SHUT_WR = 1;
|
||||
|
||||
protected static final int PF_LOCAL = 1;
|
||||
|
||||
protected static final byte AF_LOCAL = 1;
|
||||
|
||||
protected static final int SOCK_STREAM = 1;
|
||||
|
||||
private final FileDescriptor fileDescriptor;
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
private final OutputStream outputStream;
|
||||
|
||||
static {
|
||||
Native.register(Platform.C_LIBRARY_NAME);
|
||||
}
|
||||
|
||||
DomainSocket(String path) throws IOException {
|
||||
try {
|
||||
this.fileDescriptor = open(path);
|
||||
this.inputStream = new DomainSocketInputStream();
|
||||
this.outputStream = new DomainSocketOutputStream();
|
||||
}
|
||||
catch (LastErrorException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private FileDescriptor open(String path) {
|
||||
int handle = socket(PF_LOCAL, SOCK_STREAM, 0);
|
||||
connect(path, handle);
|
||||
return new FileDescriptor(handle, this::close);
|
||||
}
|
||||
|
||||
private int read(ByteBuffer buffer) throws IOException {
|
||||
try (Handle handle = this.fileDescriptor.acquire()) {
|
||||
if (handle.isClosed()) {
|
||||
return -1;
|
||||
}
|
||||
try {
|
||||
return read(handle.intValue(), buffer, buffer.remaining());
|
||||
}
|
||||
catch (LastErrorException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void write(ByteBuffer buffer) throws IOException {
|
||||
try (Handle handle = this.fileDescriptor.acquire()) {
|
||||
if (!handle.isClosed()) {
|
||||
try {
|
||||
write(handle.intValue(), buffer, buffer.remaining());
|
||||
}
|
||||
catch (LastErrorException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return this.inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
super.close();
|
||||
try {
|
||||
this.fileDescriptor.close();
|
||||
}
|
||||
catch (LastErrorException ex) {
|
||||
throw new IOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void connect(String path, int handle);
|
||||
|
||||
private native int socket(int domain, int type, int protocol) throws LastErrorException;
|
||||
|
||||
private native int read(int fd, ByteBuffer buffer, int count) throws LastErrorException;
|
||||
|
||||
private native int write(int fd, ByteBuffer buffer, int count) throws LastErrorException;
|
||||
|
||||
private native int close(int fd) throws LastErrorException;
|
||||
|
||||
/**
|
||||
* Return a new {@link DomainSocket} for the given path.
|
||||
* @param path the path to the domain socket
|
||||
* @return a {@link DomainSocket} instance
|
||||
* @throws IOException if the socket cannot be opened
|
||||
*/
|
||||
public static DomainSocket get(String path) throws IOException {
|
||||
if (Platform.isMac() || isBsdPlatform()) {
|
||||
return new BsdDomainSocket(path);
|
||||
}
|
||||
return new LinuxDomainSocket(path);
|
||||
}
|
||||
|
||||
private static boolean isBsdPlatform() {
|
||||
return Platform.isFreeBSD() || Platform.iskFreeBSD() || Platform.isNetBSD() || Platform.isOpenBSD();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InputStream} returned from the {@link DomainSocket}.
|
||||
*/
|
||||
private class DomainSocketInputStream extends InputStream {
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
int amountRead = DomainSocket.this.read(buffer);
|
||||
return (amountRead != 1) ? -1 : buffer.get() & 0xFF;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
int amountRead = DomainSocket.this.read(ByteBuffer.wrap(b, off, len));
|
||||
return (amountRead > 0) ? amountRead : -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link OutputStream} returned from the {@link DomainSocket}.
|
||||
*/
|
||||
private class DomainSocketOutputStream extends OutputStream {
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1);
|
||||
buffer.put(0, (byte) (b & 0xFF));
|
||||
DomainSocket.this.write(buffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
if (len != 0) {
|
||||
DomainSocket.this.write(ByteBuffer.wrap(b, off, len));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,123 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.function.IntConsumer;
|
||||
|
||||
/**
|
||||
* Provides access to an opaque to the underling file system representation of an open
|
||||
* file.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @see #acquire()
|
||||
*/
|
||||
class FileDescriptor {
|
||||
|
||||
private final Handle openHandle;
|
||||
|
||||
private final Handle closedHandler;
|
||||
|
||||
private final IntConsumer closer;
|
||||
|
||||
private Status status = Status.OPEN;
|
||||
|
||||
private int referenceCount;
|
||||
|
||||
FileDescriptor(int handle, IntConsumer closer) {
|
||||
this.openHandle = new Handle(handle);
|
||||
this.closedHandler = new Handle(-1);
|
||||
this.closer = closer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire an instance of the actual {@link Handle}. The caller must
|
||||
* {@link Handle#close() close} the resulting handle when done.
|
||||
* @return the handle
|
||||
*/
|
||||
synchronized Handle acquire() {
|
||||
this.referenceCount++;
|
||||
return (this.status != Status.OPEN) ? this.closedHandler : this.openHandle;
|
||||
}
|
||||
|
||||
private synchronized void release() {
|
||||
this.referenceCount--;
|
||||
if (this.referenceCount == 0 && this.status == Status.CLOSE_PENDING) {
|
||||
this.closer.accept(this.openHandle.value);
|
||||
this.status = Status.CLOSED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the underlying file when all handles have been released.
|
||||
*/
|
||||
synchronized void close() {
|
||||
if (this.status == Status.OPEN) {
|
||||
if (this.referenceCount == 0) {
|
||||
this.closer.accept(this.openHandle.value);
|
||||
this.status = Status.CLOSED;
|
||||
}
|
||||
else {
|
||||
this.status = Status.CLOSE_PENDING;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The status of the file descriptor.
|
||||
*/
|
||||
private enum Status {
|
||||
|
||||
OPEN, CLOSE_PENDING, CLOSED;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides access to the actual file descriptor handle.
|
||||
*/
|
||||
final class Handle implements Closeable {
|
||||
|
||||
private final int value;
|
||||
|
||||
private Handle(int value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
boolean isClosed() {
|
||||
return this.value == -1;
|
||||
}
|
||||
|
||||
int intValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (!isClosed()) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import com.sun.jna.LastErrorException;
|
||||
import com.sun.jna.Structure;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link DomainSocket} implementation for Linux based platforms.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LinuxDomainSocket extends DomainSocket {
|
||||
|
||||
LinuxDomainSocket(String path) throws IOException {
|
||||
super(path);
|
||||
}
|
||||
|
||||
private static final int MAX_PATH_LENGTH = 108;
|
||||
|
||||
@Override
|
||||
protected void connect(String path, int handle) {
|
||||
SockaddrUn address = new SockaddrUn(AF_LOCAL, path.getBytes(StandardCharsets.UTF_8));
|
||||
connect(handle, address, address.size());
|
||||
}
|
||||
|
||||
private native int connect(int fd, SockaddrUn address, int addressLen) throws LastErrorException;
|
||||
|
||||
/**
|
||||
* Native {@code sockaddr_un} structure as defined in {@code sys/un.h}.
|
||||
*/
|
||||
public static class SockaddrUn extends Structure implements Structure.ByReference {
|
||||
|
||||
public short sunFamily;
|
||||
|
||||
public byte[] sunPath = new byte[MAX_PATH_LENGTH];
|
||||
|
||||
private SockaddrUn(byte sunFamily, byte[] path) {
|
||||
Assert.isTrue(path.length < MAX_PATH_LENGTH, "Path cannot exceed " + MAX_PATH_LENGTH + " bytes");
|
||||
System.arraycopy(path, 0, this.sunPath, 0, path.length);
|
||||
this.sunPath[path.length] = 0;
|
||||
this.sunFamily = sunFamily;
|
||||
allocateMemory();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> getFieldOrder() {
|
||||
return Arrays.asList(new String[] { "sunLen", "sunFamily", "sunPath" });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.net.Socket;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import com.sun.jna.Platform;
|
||||
import com.sun.jna.platform.win32.Kernel32;
|
||||
|
||||
/**
|
||||
* A {@link Socket} implementation for named pipes.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class NamedPipeSocket extends Socket {
|
||||
|
||||
private static final long TIMEOUT = TimeUnit.MILLISECONDS.toNanos(1000);
|
||||
|
||||
private final RandomAccessFile file;
|
||||
|
||||
private final InputStream inputStream;
|
||||
|
||||
private final OutputStream outputStream;
|
||||
|
||||
private final Consumer<String> awaiter;
|
||||
|
||||
NamedPipeSocket(String path) throws IOException {
|
||||
this.file = open(path);
|
||||
this.inputStream = new NamedPipeInputStream();
|
||||
this.outputStream = new NamedPipeOutputStream();
|
||||
this.awaiter = Platform.isWindows() ? new WindowsAwaiter() : new SleepAwaiter();
|
||||
}
|
||||
|
||||
private RandomAccessFile open(String path) throws IOException {
|
||||
long startTime = System.nanoTime();
|
||||
while (true) {
|
||||
try {
|
||||
return new RandomAccessFile(path, "rw");
|
||||
}
|
||||
catch (FileNotFoundException ex) {
|
||||
if (System.nanoTime() - startTime > TIMEOUT) {
|
||||
throw ex;
|
||||
}
|
||||
this.awaiter.accept(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() {
|
||||
return this.inputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getOutputStream() {
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.file.close();
|
||||
}
|
||||
|
||||
protected final RandomAccessFile getFile() {
|
||||
return this.file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a new {@link NamedPipeSocket} for the given path.
|
||||
* @param path the path to the domain socket
|
||||
* @return a {@link NamedPipeSocket} instance
|
||||
* @throws IOException if the socket cannot be opened
|
||||
*/
|
||||
public static NamedPipeSocket get(String path) throws IOException {
|
||||
return new NamedPipeSocket(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InputStream} returned from the {@link NamedPipeSocket}.
|
||||
*/
|
||||
private class NamedPipeInputStream extends InputStream {
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return getFile().read();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] bytes, int off, int len) throws IOException {
|
||||
return getFile().read(bytes, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link InputStream} returned from the {@link NamedPipeSocket}.
|
||||
*/
|
||||
private class NamedPipeOutputStream extends OutputStream {
|
||||
|
||||
@Override
|
||||
public void write(int value) throws IOException {
|
||||
NamedPipeSocket.this.file.write(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] bytes, int off, int len) throws IOException {
|
||||
NamedPipeSocket.this.file.write(bytes, off, len);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the name pipe file using a simple sleep.
|
||||
*/
|
||||
private class SleepAwaiter implements Consumer<String> {
|
||||
|
||||
@Override
|
||||
public void accept(String path) {
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the name pipe file using Windows specific logic.
|
||||
*/
|
||||
private class WindowsAwaiter implements Consumer<String> {
|
||||
|
||||
@Override
|
||||
public void accept(String path) {
|
||||
Kernel32.INSTANCE.WaitNamedPipe(path, 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Low-level {@link java.net.Socket} implementations required for local Docker access.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.socket;
|
@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.toml;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Very simple TOML markup builder.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.3.0
|
||||
*/
|
||||
public class Toml {
|
||||
|
||||
private final StringBuilder toml = new StringBuilder();
|
||||
|
||||
public void table(String name) {
|
||||
append("[" + name + "]");
|
||||
}
|
||||
|
||||
public void string(String name, String value) {
|
||||
append(name + " = " + quote(value));
|
||||
}
|
||||
|
||||
public void array(String name, String... value) {
|
||||
if (value != null && value.length > 0) {
|
||||
append(name + " = " + Arrays.stream(value).map(this::quote).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
private void append(String line) {
|
||||
this.toml.append(line).append('\n');
|
||||
}
|
||||
|
||||
private String quote(String string) {
|
||||
return "\"" + string + "\"";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.toml.toString();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Support for writing TOML content.
|
||||
*/
|
||||
package org.springframework.boot.cloudnativebuildpack.toml;
|
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ApiVersion}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ApiVersionTests {
|
||||
|
||||
@Test
|
||||
void parseWhenVersionIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse(null))
|
||||
.withMessage("Value must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWhenVersionIsEmptyThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse(""))
|
||||
.withMessage("Value must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWhenVersionDoesNotMatchPatternThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ApiVersion.parse("bad"))
|
||||
.withMessage("Malformed version number 'bad'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseReturnsVersion() {
|
||||
ApiVersion version = ApiVersion.parse("1.2");
|
||||
assertThat(version.getMajor()).isEqualTo(1);
|
||||
assertThat(version.getMinor()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenSupports() {
|
||||
ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.0"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertSupportsWhenDoesNotSupportThrowsException() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> ApiVersion.parse("1.2").assertSupports(ApiVersion.parse("1.3")))
|
||||
.withMessage("Version 'v1.3' is not supported by this version ('v1.2')");
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportWhenSame() {
|
||||
assertThat(supports("0.0", "0.0")).isTrue();
|
||||
assertThat(supports("0.1", "0.1")).isTrue();
|
||||
assertThat(supports("1.0", "1.0")).isTrue();
|
||||
assertThat(supports("1.1", "1.1")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsWhenDifferentMajor() {
|
||||
assertThat(supports("0.0", "1.0")).isFalse();
|
||||
assertThat(supports("1.0", "0.0")).isFalse();
|
||||
assertThat(supports("1.0", "2.0")).isFalse();
|
||||
assertThat(supports("2.0", "1.0")).isFalse();
|
||||
assertThat(supports("1.1", "2.1")).isFalse();
|
||||
assertThat(supports("2.1", "1.1")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportsWhenDifferentMinor() {
|
||||
assertThat(supports("1.2", "1.1")).isTrue();
|
||||
assertThat(supports("1.2", "1.3")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void supportWhenMajorZeroAndDifferentMinor() {
|
||||
assertThat(supports("0.2", "0.1")).isFalse();
|
||||
assertThat(supports("0.2", "0.3")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsString() {
|
||||
assertThat(ApiVersion.parse("1.2").toString()).isEqualTo("v1.2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsAndHashCode() {
|
||||
ApiVersion v12a = ApiVersion.parse("1.2");
|
||||
ApiVersion v12b = ApiVersion.parse("1.2");
|
||||
ApiVersion v13 = ApiVersion.parse("1.3");
|
||||
assertThat(v12a.hashCode()).isEqualTo(v12b.hashCode());
|
||||
assertThat(v12a).isEqualTo(v12a).isEqualTo(v12b).isNotEqualTo(v13);
|
||||
}
|
||||
|
||||
private boolean supports(String v1, String v2) {
|
||||
return ApiVersion.parse(v1).supports(ApiVersion.parse(v2));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link BuildLog}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuildLogTests {
|
||||
|
||||
@Test
|
||||
void toSystemOutPrintsToSystemOut() {
|
||||
BuildLog log = BuildLog.toSystemOut();
|
||||
assertThat(log).isInstanceOf(PrintStreamBuildLog.class);
|
||||
assertThat(log).extracting("out").isSameAs(System.out);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toPrintsToOutput() {
|
||||
BuildLog log = BuildLog.to(System.err);
|
||||
assertThat(log).isInstanceOf(PrintStreamBuildLog.class);
|
||||
assertThat(log).extracting("out").isSameAs(System.err);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* Tests for {@link BuildOwner}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuildOwnerTests {
|
||||
|
||||
@Test
|
||||
void fromEnvReturnsOwner() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("CNB_USER_ID", "123");
|
||||
env.put("CNB_GROUP_ID", "456");
|
||||
BuildOwner owner = BuildOwner.fromEnv(env);
|
||||
assertThat(owner.getUid()).isEqualTo(123);
|
||||
assertThat(owner.getGid()).isEqualTo(456);
|
||||
assertThat(owner.toString()).isEqualTo("123/456");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromEnvWhenEnvIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuildOwner.fromEnv(null))
|
||||
.withMessage("Env must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromEnvWhenUserPropertyIsMissingThrowsException() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("CNB_GROUP_ID", "456");
|
||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||
.withMessage("Missing 'CNB_USER_ID' value from the builder environment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromEnvWhenGroupPropertyIsMissingThrowsException() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("CNB_USER_ID", "123");
|
||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||
.withMessage("Missing 'CNB_GROUP_ID' value from the builder environment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromEnvWhenUserPropertyIsMalformedThrowsException() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("CNB_USER_ID", "nope");
|
||||
env.put("CNB_GROUP_ID", "456");
|
||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||
.withMessage("Malformed 'CNB_USER_ID' value 'nope' in the builder environment");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromEnvWhenGroupPropertyIsMalformedThrowsException() {
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("CNB_USER_ID", "123");
|
||||
env.put("CNB_GROUP_ID", "nope");
|
||||
assertThatIllegalStateException().isThrownBy(() -> BuildOwner.fromEnv(env))
|
||||
.withMessage("Malformed 'CNB_GROUP_ID' value 'nope' in the builder environment");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.entry;
|
||||
|
||||
/**
|
||||
* Tests for {@link BuildRequest}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
public class BuildRequestTests {
|
||||
|
||||
@TempDir
|
||||
File tempDir;
|
||||
|
||||
@Test
|
||||
void forJarFileReturnsRequest() throws IOException {
|
||||
File jarFile = new File(this.tempDir, "my-app-0.0.1.jar");
|
||||
writeTestJarFile(jarFile);
|
||||
BuildRequest request = BuildRequest.forJarFile(jarFile);
|
||||
assertThat(request.getName().toString()).isEqualTo("docker.io/library/my-app:0.0.1");
|
||||
assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic");
|
||||
assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent);
|
||||
assertThat(request.getEnv()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void forJarFileWithNameReturnsRequest() throws IOException {
|
||||
File jarFile = new File(this.tempDir, "my-app-0.0.1.jar");
|
||||
writeTestJarFile(jarFile);
|
||||
BuildRequest request = BuildRequest.forJarFile(ImageReference.of("test-app"), jarFile);
|
||||
assertThat(request.getName().toString()).isEqualTo("docker.io/library/test-app:latest");
|
||||
assertThat(request.getBuilder().toString()).isEqualTo("docker.io/cloudfoundry/cnb:0.0.43-bionic");
|
||||
assertThat(request.getApplicationContent(Owner.ROOT)).satisfies(this::hasExpectedJarContent);
|
||||
assertThat(request.getEnv()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void forJarFileWhenJarFileIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(null))
|
||||
.withMessage("JarFile must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forJarFileWhenJarFileIsMissingThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> BuildRequest.forJarFile(new File(this.tempDir, "missing.jar")))
|
||||
.withMessage("JarFile must exist");
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
void forJarFileWhenJarFileIsFolderThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuildRequest.forJarFile(this.tempDir))
|
||||
.withMessage("JarFile must be a file");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withBuilderUpdatesBuilder() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"))
|
||||
.withBuilder(ImageReference.of("spring/builder"));
|
||||
assertThat(request.getBuilder().toString()).isEqualTo("docker.io/spring/builder:latest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnvAddsEnvEntry() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
BuildRequest withEnv = request.withEnv("spring", "boot");
|
||||
assertThat(request.getEnv()).isEmpty();
|
||||
assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnvMapAddsEnvEntries() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
Map<String, String> env = new LinkedHashMap<>();
|
||||
env.put("spring", "boot");
|
||||
env.put("test", "test");
|
||||
BuildRequest withEnv = request.withEnv(env);
|
||||
assertThat(request.getEnv()).isEmpty();
|
||||
assertThat(withEnv.getEnv()).containsExactly(entry("spring", "boot"), entry("test", "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnvWhenKeyIsNullThrowsException() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv(null, "test"))
|
||||
.withMessage("Name must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnvWhenValueIsNullThrowsException() throws IOException {
|
||||
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> request.withEnv("test", null))
|
||||
.withMessage("Value must not be empty");
|
||||
}
|
||||
|
||||
private void hasExpectedJarContent(TarArchive archive) {
|
||||
try {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
archive.writeTo(outputStream);
|
||||
try (TarArchiveInputStream tar = new TarArchiveInputStream(
|
||||
new ByteArrayInputStream(outputStream.toByteArray()))) {
|
||||
assertThat(tar.getNextEntry().getName()).isEqualTo("spring/");
|
||||
assertThat(tar.getNextEntry().getName()).isEqualTo("spring/boot");
|
||||
assertThat(tar.getNextEntry()).isNull();
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private File writeTestJarFile(String name) throws IOException {
|
||||
File file = new File(this.tempDir, name);
|
||||
writeTestJarFile(file);
|
||||
return file;
|
||||
}
|
||||
|
||||
private void writeTestJarFile(File file) throws IOException {
|
||||
try (ZipArchiveOutputStream zip = new ZipArchiveOutputStream(file)) {
|
||||
ZipArchiveEntry dirEntry = new ZipArchiveEntry("spring/");
|
||||
zip.putArchiveEntry(dirEntry);
|
||||
zip.closeArchiveEntry();
|
||||
ZipArchiveEntry fileEntry = new ZipArchiveEntry("spring/boot");
|
||||
zip.putArchiveEntry(fileEntry);
|
||||
zip.write("test".getBytes(StandardCharsets.UTF_8));
|
||||
zip.closeArchiveEntry();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,97 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link BuilderMetadata}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuilderMetadataTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void fromImageLoadsMetadata() throws IOException {
|
||||
Image image = Image.of(getContent("image.json"));
|
||||
BuilderMetadata metadata = BuilderMetadata.fromImage(image);
|
||||
assertThat(metadata.getStack().getRunImage().getImage()).isEqualTo("cloudfoundry/run:full-cnb");
|
||||
assertThat(metadata.getStack().getRunImage().getMirrors()).isEmpty();
|
||||
assertThat(metadata.getLifecycle().getVersion()).isEqualTo("0.5.0");
|
||||
assertThat(metadata.getLifecycle().getApi().getBuildpack()).isEqualTo("0.2");
|
||||
assertThat(metadata.getLifecycle().getApi().getPlatform()).isEqualTo("0.1");
|
||||
assertThat(metadata.getCreatedBy().getName()).isEqualTo("Pack CLI");
|
||||
assertThat(metadata.getCreatedBy().getVersion())
|
||||
.isEqualTo("v0.5.0 (git sha: c9cfac75b49609524e1ea33f809c12071406547c)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromImageWhenImageIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(null))
|
||||
.withMessage("Image must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromImageWhenImageConfigIsNullThrowsException() {
|
||||
Image image = mock(Image.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image))
|
||||
.withMessage("ImageConfig must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromImageConfigWhenLabelIsMissingThrowsException() {
|
||||
Image image = mock(Image.class);
|
||||
ImageConfig imageConfig = mock(ImageConfig.class);
|
||||
given(image.getConfig()).willReturn(imageConfig);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> BuilderMetadata.fromImage(image))
|
||||
.withMessage("No 'io.buildpacks.builder.metadata' label found in image config");
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyWithUpdatedCreatedByReturnsNewMetadata() throws IOException {
|
||||
Image image = Image.of(getContent("image.json"));
|
||||
BuilderMetadata metadata = BuilderMetadata.fromImage(image);
|
||||
BuilderMetadata copy = metadata.copy((update) -> update.withCreatedBy("test123", "test456"));
|
||||
assertThat(copy).isNotSameAs(metadata);
|
||||
assertThat(copy.getCreatedBy().getName()).isEqualTo("test123");
|
||||
assertThat(copy.getCreatedBy().getVersion()).isEqualTo("test456");
|
||||
}
|
||||
|
||||
@Test
|
||||
void attachToUpdatesMetadata() throws IOException {
|
||||
Image image = Image.of(getContent("image.json"));
|
||||
ImageConfig imageConfig = image.getConfig();
|
||||
BuilderMetadata metadata = BuilderMetadata.fromImage(image);
|
||||
ImageConfig imageConfigCopy = imageConfig.copy(metadata::attachTo);
|
||||
String label = imageConfigCopy.getLabels().get("io.buildpacks.builder.metadata");
|
||||
BuilderMetadata metadataCopy = BuilderMetadata.fromJson(label);
|
||||
assertThat(metadataCopy.getStack().getRunImage().getImage())
|
||||
.isEqualTo(metadata.getStack().getRunImage().getImage());
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressPullListener;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link Builder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class BuilderTests {
|
||||
|
||||
@Test
|
||||
void createWhenLogIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new Builder(null)).withMessage("Log must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenRequestIsNullThrowsException() {
|
||||
Builder builder = new Builder();
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> builder.build(null))
|
||||
.withMessage("Request must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildInvokesBuildpack() throws Exception {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image.json");
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/cnb:0.0.43-bionic")), any()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:full-cnb")), any()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
BuildRequest request = getTestRequest();
|
||||
builder.build(request);
|
||||
assertThat(out.toString()).contains("Running detector");
|
||||
assertThat(out.toString()).contains("Running restorer");
|
||||
assertThat(out.toString()).contains("Running analyzer");
|
||||
assertThat(out.toString()).contains("Running builder");
|
||||
assertThat(out.toString()).contains("Running exporter");
|
||||
assertThat(out.toString()).contains("Running cacher");
|
||||
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
|
||||
verify(docker.image()).load(archive.capture(), any());
|
||||
verify(docker.image()).remove(archive.getValue().getTag(), true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
DockerApi docker = mockDockerApi();
|
||||
Image builderImage = loadImage("image.json");
|
||||
Image runImage = loadImage("run-image-with-bad-stack.json");
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/cnb:0.0.43-bionic")), any()))
|
||||
.willAnswer(withPulledImage(builderImage));
|
||||
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:full-cnb")), any()))
|
||||
.willAnswer(withPulledImage(runImage));
|
||||
Builder builder = new Builder(BuildLog.to(out), docker);
|
||||
BuildRequest request = getTestRequest();
|
||||
assertThatIllegalStateException().isThrownBy(() -> builder.build(request)).withMessage(
|
||||
"Run image stack 'org.cloudfoundry.stacks.cfwindowsfs3' does not match builder stack 'org.cloudfoundry.stacks.cflinuxfs3'");
|
||||
}
|
||||
|
||||
private DockerApi mockDockerApi() {
|
||||
DockerApi docker = mock(DockerApi.class);
|
||||
ImageApi imageApi = mock(ImageApi.class);
|
||||
ContainerApi containerApi = mock(ContainerApi.class);
|
||||
VolumeApi volumeApi = mock(VolumeApi.class);
|
||||
given(docker.image()).willReturn(imageApi);
|
||||
given(docker.container()).willReturn(containerApi);
|
||||
given(docker.volume()).willReturn(volumeApi);
|
||||
return docker;
|
||||
}
|
||||
|
||||
private BuildRequest getTestRequest() {
|
||||
TarArchive content = mock(TarArchive.class);
|
||||
ImageReference name = ImageReference.of("my-application");
|
||||
BuildRequest request = BuildRequest.of(name, (owner) -> content);
|
||||
return request;
|
||||
}
|
||||
|
||||
private Image loadImage(String name) throws IOException {
|
||||
return Image.of(getClass().getResourceAsStream(name));
|
||||
}
|
||||
|
||||
private Answer<Image> withPulledImage(Image image) {
|
||||
return (invocation) -> {
|
||||
TotalProgressPullListener listener = invocation.getArgument(1, TotalProgressPullListener.class);
|
||||
listener.onStart();
|
||||
listener.onFinish();
|
||||
return image;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
static class TestPrintStream extends PrintStream {
|
||||
|
||||
TestPrintStream() {
|
||||
super(new ByteArrayOutputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.compress.archivers.ArchiveEntry;
|
||||
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
|
||||
import org.apache.commons.compress.utils.IOUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link EphemeralBuilder}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class EphemeralBuilderTests extends AbstractJsonTests {
|
||||
|
||||
@TempDir
|
||||
File temp;
|
||||
|
||||
private final BuildOwner owner = BuildOwner.of(123, 456);
|
||||
|
||||
private Image image;
|
||||
|
||||
private BuilderMetadata metadata;
|
||||
|
||||
private Map<String, String> env;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
this.image = Image.of(getContent("image.json"));
|
||||
this.metadata = BuilderMetadata.fromImage(this.image);
|
||||
this.env = Collections.singletonMap("spring", "boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNameHasRandomName() throws Exception {
|
||||
EphemeralBuilder b1 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
EphemeralBuilder b2 = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
assertThat(b1.getName().toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
||||
assertThat(b1.getName().toString()).isNotEqualTo(b2.getName().toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveHasCreatedByConfig() throws Exception {
|
||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
ImageConfig config = builder.getArchive().getImageConfig();
|
||||
BuilderMetadata ephemeralMetadata = BuilderMetadata.fromImageConfig(config);
|
||||
assertThat(ephemeralMetadata.getCreatedBy().getName()).isEqualTo("Spring Boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveHasTag() throws Exception {
|
||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
ImageReference tag = builder.getArchive().getTag();
|
||||
assertThat(tag.toString()).startsWith("pack.local/builder/").endsWith(":latest");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveHasCreateDate() throws Exception {
|
||||
Clock clock = Clock.fixed(Instant.now(), ZoneOffset.UTC);
|
||||
EphemeralBuilder builder = new EphemeralBuilder(clock, this.owner, this.image, this.metadata, this.env);
|
||||
assertThat(builder.getArchive().getCreateDate()).isEqualTo(Instant.now(clock));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveContainsDefaultDirsLayer() throws Exception {
|
||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
File folder = unpack(getLayer(builder.getArchive(), 0), "dirs");
|
||||
assertThat(new File(folder, "workspace")).isDirectory();
|
||||
assertThat(new File(folder, "layers")).isDirectory();
|
||||
assertThat(new File(folder, "cnb")).isDirectory();
|
||||
assertThat(new File(folder, "cnb/buildpacks")).isDirectory();
|
||||
assertThat(new File(folder, "platform")).isDirectory();
|
||||
assertThat(new File(folder, "platform/env")).isDirectory();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveContainsStackLayer() throws Exception {
|
||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
File folder = unpack(getLayer(builder.getArchive(), 1), "stack");
|
||||
File tomlFile = new File(folder, "cnb/stack.toml");
|
||||
assertThat(tomlFile).exists();
|
||||
String toml = FileCopyUtils
|
||||
.copyToString(new InputStreamReader(new FileInputStream(tomlFile), StandardCharsets.UTF_8));
|
||||
assertThat(toml).contains("[run-image]").contains("image = ");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArchiveContainsEnvLayer() throws Exception {
|
||||
EphemeralBuilder builder = new EphemeralBuilder(this.owner, this.image, this.metadata, this.env);
|
||||
File folder = unpack(getLayer(builder.getArchive(), 2), "env");
|
||||
assertThat(new File(folder, "platform/env/spring")).usingCharset(StandardCharsets.UTF_8).hasContent("boot");
|
||||
}
|
||||
|
||||
private TarArchiveInputStream getLayer(ImageArchive archive, int index) throws Exception {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
archive.writeTo(outputStream);
|
||||
TarArchiveInputStream tar = new TarArchiveInputStream(new ByteArrayInputStream(outputStream.toByteArray()));
|
||||
for (int i = 0; i <= index; i++) {
|
||||
tar.getNextEntry();
|
||||
}
|
||||
return new TarArchiveInputStream(tar);
|
||||
}
|
||||
|
||||
private File unpack(TarArchiveInputStream archive, String name) throws Exception {
|
||||
File folder = new File(this.temp, name);
|
||||
folder.mkdirs();
|
||||
ArchiveEntry entry = archive.getNextEntry();
|
||||
while (entry != null) {
|
||||
File file = new File(folder, entry.getName());
|
||||
if (entry.isDirectory()) {
|
||||
file.mkdirs();
|
||||
}
|
||||
else {
|
||||
file.getParentFile().mkdirs();
|
||||
try (OutputStream out = new FileOutputStream(file)) {
|
||||
IOUtils.copy(archive, out);
|
||||
}
|
||||
}
|
||||
entry = archive.getNextEntry();
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.SharedObjectMapper;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link Lifecycle}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LifecycleTests {
|
||||
|
||||
private TestPrintStream out;
|
||||
|
||||
private DockerApi docker;
|
||||
|
||||
private Lifecycle lifecycle;
|
||||
|
||||
private Map<String, ContainerConfig> configs = new LinkedHashMap<>();
|
||||
|
||||
private Map<String, ContainerContent> content = new LinkedHashMap<>();
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
this.out = new TestPrintStream();
|
||||
this.docker = mockDockerApi();
|
||||
BuildRequest request = getTestRequest();
|
||||
this.lifecycle = createLifecycle(request);
|
||||
}
|
||||
|
||||
private DockerApi mockDockerApi() {
|
||||
DockerApi docker = mock(DockerApi.class);
|
||||
ImageApi imageApi = mock(ImageApi.class);
|
||||
ContainerApi containerApi = mock(ContainerApi.class);
|
||||
VolumeApi volumeApi = mock(VolumeApi.class);
|
||||
given(docker.image()).willReturn(imageApi);
|
||||
given(docker.container()).willReturn(containerApi);
|
||||
given(docker.volume()).willReturn(volumeApi);
|
||||
return docker;
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeExecutesPhases() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
this.lifecycle.execute();
|
||||
assertPhaseWasRun("detector", withExpectedConfig("lifecycle-detector.json"));
|
||||
assertPhaseWasRun("restorer", withExpectedConfig("lifecycle-restorer.json"));
|
||||
assertPhaseWasRun("analyzer", withExpectedConfig("lifecycle-analyzer.json"));
|
||||
assertPhaseWasRun("builder", withExpectedConfig("lifecycle-builder.json"));
|
||||
assertPhaseWasRun("exporter", withExpectedConfig("lifecycle-exporter.json"));
|
||||
assertPhaseWasRun("cacher", withExpectedConfig("lifecycle-cacher.json"));
|
||||
assertThat(this.out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeOnlyUploadsContentOnce() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
this.lifecycle.execute();
|
||||
assertThat(this.content).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenAleadyRunThrowsException() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
this.lifecycle.execute();
|
||||
assertThatIllegalStateException().isThrownBy(this.lifecycle::execute)
|
||||
.withMessage("Lifecycle has already been executed");
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenCleanCacheClearsCache() throws Exception {
|
||||
given(this.docker.container().create(any())).willAnswer(answerWithGeneratedContainerId());
|
||||
given(this.docker.container().create(any(), any())).willAnswer(answerWithGeneratedContainerId());
|
||||
BuildRequest request = getTestRequest().withCleanCache(true);
|
||||
createLifecycle(request).execute();
|
||||
VolumeName name = VolumeName.of("pack-cache-b35197ac41ea.build");
|
||||
verify(this.docker.volume()).delete(name, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void closeClearsVolumes() throws Exception {
|
||||
this.lifecycle.close();
|
||||
verify(this.docker.volume()).delete(VolumeName.of("pack-layers-aaaaaaaaaa"), true);
|
||||
verify(this.docker.volume()).delete(VolumeName.of("pack-app-aaaaaaaaaa"), true);
|
||||
}
|
||||
|
||||
private BuildRequest getTestRequest() {
|
||||
TarArchive content = mock(TarArchive.class);
|
||||
ImageReference name = ImageReference.of("my-application");
|
||||
BuildRequest request = BuildRequest.of(name, (owner) -> content);
|
||||
return request;
|
||||
}
|
||||
|
||||
private Lifecycle createLifecycle(BuildRequest request) throws IOException {
|
||||
EphemeralBuilder builder = mockEphemeralBuilder();
|
||||
return new TestLifecycle(BuildLog.to(this.out), this.docker, request, ImageReference.of("cloudfoundry/run"),
|
||||
builder);
|
||||
}
|
||||
|
||||
private EphemeralBuilder mockEphemeralBuilder() throws IOException {
|
||||
EphemeralBuilder builder = mock(EphemeralBuilder.class);
|
||||
byte[] metadataContent = FileCopyUtils.copyToByteArray(getClass().getResourceAsStream("builder-metadata.json"));
|
||||
BuilderMetadata metadata = BuilderMetadata.fromJson(new String(metadataContent, StandardCharsets.UTF_8));
|
||||
given(builder.getName()).willReturn(ImageReference.of("pack.local/ephemeral-builder"));
|
||||
given(builder.getBuilderMetadata()).willReturn(metadata);
|
||||
return builder;
|
||||
}
|
||||
|
||||
private Answer<ContainerReference> answerWithGeneratedContainerId() {
|
||||
return (invocation) -> {
|
||||
ContainerConfig config = invocation.getArgument(0, ContainerConfig.class);
|
||||
ArrayNode command = getCommand(config);
|
||||
String name = command.get(0).asText().substring(1).replaceAll("/", "-");
|
||||
this.configs.put(name, config);
|
||||
if (invocation.getArguments().length > 1) {
|
||||
this.content.put(name, invocation.getArgument(1, ContainerContent.class));
|
||||
}
|
||||
return ContainerReference.of(name);
|
||||
};
|
||||
}
|
||||
|
||||
private ArrayNode getCommand(ContainerConfig config) throws JsonProcessingException, JsonMappingException {
|
||||
JsonNode node = SharedObjectMapper.get().readTree(config.toString());
|
||||
return (ArrayNode) node.at("/Cmd");
|
||||
}
|
||||
|
||||
private void assertPhaseWasRun(String name, IOConsumer<ContainerConfig> configConsumer) throws IOException {
|
||||
ContainerReference containerReference = ContainerReference.of("lifecycle-" + name);
|
||||
verify(this.docker.container()).start(containerReference);
|
||||
verify(this.docker.container()).logs(eq(containerReference), any());
|
||||
verify(this.docker.container()).remove(containerReference, true);
|
||||
configConsumer.accept(this.configs.get(containerReference.toString()));
|
||||
}
|
||||
|
||||
private IOConsumer<ContainerConfig> withExpectedConfig(String name) {
|
||||
return (config) -> {
|
||||
InputStream in = getClass().getResourceAsStream(name);
|
||||
String json = FileCopyUtils.copyToString(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
assertThat(config.toString()).isEqualToIgnoringWhitespace(json);
|
||||
};
|
||||
}
|
||||
|
||||
static class TestLifecycle extends Lifecycle {
|
||||
|
||||
TestLifecycle(BuildLog log, DockerApi docker, BuildRequest request, ImageReference runImageReferece,
|
||||
EphemeralBuilder builder) {
|
||||
super(log, docker, request, runImageReferece, builder);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected VolumeName createRandomVolumeName(String prefix) {
|
||||
return VolumeName.of(prefix + "aaaaaaaaaa");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class TestPrintStream extends PrintStream {
|
||||
|
||||
TestPrintStream() {
|
||||
super(new ByteArrayOutputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link LifecycleVersion}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LifecycleVersionTests {
|
||||
|
||||
@Test
|
||||
void parseWhenValueIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse(null))
|
||||
.withMessage("Value must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWhenTooLongThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3.4"))
|
||||
.withMessage("Malformed version number '1.2.3.4'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseWhenNonNumericThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> LifecycleVersion.parse("v1.2.3a"))
|
||||
.withMessage("Malformed version number '1.2.3a'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compareTo() {
|
||||
LifecycleVersion v4 = LifecycleVersion.parse("0.0.4");
|
||||
assertThat(LifecycleVersion.parse("0.0.3").compareTo(v4)).isNegative();
|
||||
assertThat(LifecycleVersion.parse("0.0.4").compareTo(v4)).isZero();
|
||||
assertThat(LifecycleVersion.parse("0.0.5").compareTo(v4)).isPositive();
|
||||
}
|
||||
|
||||
@Test
|
||||
void isEqualOrGreaterThan() {
|
||||
LifecycleVersion v4 = LifecycleVersion.parse("0.0.4");
|
||||
assertThat(LifecycleVersion.parse("0.0.3").isEqualOrGreaterThan(v4)).isFalse();
|
||||
assertThat(LifecycleVersion.parse("0.0.4").isEqualOrGreaterThan(v4)).isTrue();
|
||||
assertThat(LifecycleVersion.parse("0.0.5").isEqualOrGreaterThan(v4)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseReturnsVersion() {
|
||||
assertThat(LifecycleVersion.parse("1.2.3").toString()).isEqualTo("v1.2.3");
|
||||
assertThat(LifecycleVersion.parse("1.2").toString()).isEqualTo("v1.2.0");
|
||||
assertThat(LifecycleVersion.parse("1").toString()).isEqualTo("v1.0.0");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig.Update;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
|
||||
/**
|
||||
* Tests for {@link Phase}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PhaseTests {
|
||||
|
||||
private static final String[] NO_ARGS = {};
|
||||
|
||||
@Test
|
||||
void getNameReturnsName() {
|
||||
Phase phase = new Phase("test", false);
|
||||
assertThat(phase.getName()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsName() {
|
||||
Phase phase = new Phase("test", false);
|
||||
assertThat(phase).hasToString("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyUpdatesConfiguration() {
|
||||
Phase phase = new Phase("test", false);
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/lifecycle/test", NO_ARGS);
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithDaemonAccessUpdatesConfigurationWithRootUserAndDomainSocketBinding() {
|
||||
Phase phase = new Phase("test", false);
|
||||
phase.withDaemonAccess();
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withUser("root");
|
||||
verify(update).withBind("/var/run/docker.sock", "/var/run/docker.sock");
|
||||
verify(update).withCommand("/lifecycle/test", NO_ARGS);
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithLogLevelArgAndVerboseLoggingUpdatesConfigurationWithLogLevel() {
|
||||
Phase phase = new Phase("test", true);
|
||||
phase.withLogLevelArg();
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/lifecycle/test", "-log-level", "debug");
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithLogLevelArgAndNonVerboseLoggingDoesNotUpdateLogLevel() {
|
||||
Phase phase = new Phase("test", false);
|
||||
phase.withLogLevelArg();
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/lifecycle/test");
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithArgsUpdatesConfigurationWithArguments() {
|
||||
Phase phase = new Phase("test", false);
|
||||
phase.withArgs("a", "b", "c");
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/lifecycle/test", "a", "b", "c");
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyWhenWithBindsUpdatesConfigurationWithBinds() {
|
||||
Phase phase = new Phase("test", false);
|
||||
VolumeName volumeName = VolumeName.of("test");
|
||||
phase.withBinds(volumeName, "/test");
|
||||
Update update = mock(Update.class);
|
||||
phase.apply(update);
|
||||
verify(update).withCommand("/lifecycle/test");
|
||||
verify(update).withLabel("author", "spring-boot");
|
||||
verify(update).withBind(volumeName, "/test");
|
||||
verifyNoMoreInteractions(update);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.LogUpdateEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.TotalProgressEvent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link PrintStreamBuildLog}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PrintStreamBuildLogTests {
|
||||
|
||||
@Test
|
||||
void printsExpectedOutput() throws Exception {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
PrintStreamBuildLog log = new PrintStreamBuildLog(out);
|
||||
BuildRequest request = mock(BuildRequest.class);
|
||||
ImageReference name = ImageReference.of("my-app:latest");
|
||||
ImageReference builderImageReference = ImageReference.of("cnb/builder");
|
||||
Image builderImage = mock(Image.class);
|
||||
given(builderImage.getDigests()).willReturn(Collections.singletonList("00000001"));
|
||||
ImageReference runImageReference = ImageReference.of("cnb/runner");
|
||||
Image runImage = mock(Image.class);
|
||||
given(runImage.getDigests()).willReturn(Collections.singletonList("00000002"));
|
||||
given(request.getName()).willReturn(name);
|
||||
log.start(request);
|
||||
Consumer<TotalProgressEvent> pullBuildImageConsumer = log.pullingBuilder(request, builderImageReference);
|
||||
pullBuildImageConsumer.accept(new TotalProgressEvent(100));
|
||||
log.pulledBulder(request, builderImage);
|
||||
Consumer<TotalProgressEvent> pullRunImageConsumer = log.pullingRunImage(request, runImageReference);
|
||||
pullRunImageConsumer.accept(new TotalProgressEvent(100));
|
||||
log.pulledRunImage(request, runImage);
|
||||
log.executingLifecycle(request, LifecycleVersion.parse("0.5"), VolumeName.of("pack-abc.cache"));
|
||||
Consumer<LogUpdateEvent> phase1Consumer = log.runningPhase(request, "alphabet");
|
||||
phase1Consumer.accept(mockLogEvent("one"));
|
||||
phase1Consumer.accept(mockLogEvent("two"));
|
||||
phase1Consumer.accept(mockLogEvent("three"));
|
||||
Consumer<LogUpdateEvent> phase2Consumer = log.runningPhase(request, "basket");
|
||||
phase2Consumer.accept(mockLogEvent("spring"));
|
||||
phase2Consumer.accept(mockLogEvent("boot"));
|
||||
log.executedLifecycle(request);
|
||||
String expected = FileCopyUtils.copyToString(new InputStreamReader(
|
||||
getClass().getResourceAsStream("print-stream-build-log.txt"), StandardCharsets.UTF_8));
|
||||
assertThat(out.toString()).isEqualToIgnoringNewLines(expected);
|
||||
}
|
||||
|
||||
private LogUpdateEvent mockLogEvent(String string) {
|
||||
LogUpdateEvent event = mock(LogUpdateEvent.class);
|
||||
given(event.toString()).willReturn(string);
|
||||
return event;
|
||||
}
|
||||
|
||||
static class TestPrintStream extends PrintStream {
|
||||
|
||||
TestPrintStream() {
|
||||
super(new ByteArrayOutputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.build;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageConfig;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link StackId}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class StackIdTests {
|
||||
|
||||
@Test
|
||||
void fromImageWhenImageIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> StackId.fromImage(null))
|
||||
.withMessage("Image must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromImageWhenLabelIsMissingThrowsException() {
|
||||
Image image = mock(Image.class);
|
||||
ImageConfig imageConfig = mock(ImageConfig.class);
|
||||
given(image.getConfig()).willReturn(imageConfig);
|
||||
assertThatIllegalStateException().isThrownBy(() -> StackId.fromImage(image))
|
||||
.withMessage("Missing 'io.buildpacks.stack.id' stack label");
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromImageCreatesStackId() {
|
||||
Image image = mock(Image.class);
|
||||
ImageConfig imageConfig = mock(ImageConfig.class);
|
||||
given(image.getConfig()).willReturn(imageConfig);
|
||||
given(imageConfig.getLabels()).willReturn(Collections.singletonMap("io.buildpacks.stack.id", "test"));
|
||||
StackId stackId = StackId.fromImage(image);
|
||||
assertThat(stackId.toString()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofCreatesStackId() {
|
||||
StackId stackId = StackId.of("test");
|
||||
assertThat(stackId.toString()).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void equalsAndHashCode() {
|
||||
StackId s1 = StackId.of("a");
|
||||
StackId s2 = StackId.of("a");
|
||||
StackId s3 = StackId.of("b");
|
||||
assertThat(s1.hashCode()).isEqualTo(s2.hashCode());
|
||||
assertThat(s1).isEqualTo(s1).isEqualTo(s2).isNotEqualTo(s3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringReturnsValue() {
|
||||
StackId stackId = StackId.of("test");
|
||||
assertThat(stackId.toString()).isEqualTo("test");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.testsupport.testcontainers.DisabledIfDockerUnavailable;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link DockerApi}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
@DisabledIfDockerUnavailable
|
||||
class DockerApiIntegrationTests {
|
||||
|
||||
private final DockerApi docker = new DockerApi();
|
||||
|
||||
@Test
|
||||
void pullImage() throws IOException {
|
||||
this.docker.image().pull(ImageReference.of("cloudfoundry/cnb:bionic"),
|
||||
new TotalProgressPullListener(new TotalProgressBar("Pulling: ")));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,393 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URI;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.InOrder;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ContainerApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.ImageApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.DockerApi.VolumeApi;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.Http.Response;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerConfig;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerContent;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ContainerReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.Image;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageArchive;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.ImageReference;
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.type.VolumeName;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Content;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.IOConsumer;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.Owner;
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.inOrder;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link DockerApi}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerApiTests {
|
||||
|
||||
private static final String API_URL = "docker://localhost/v1.40";
|
||||
|
||||
private static final String IMAGES_URL = API_URL + "/images";
|
||||
|
||||
private static final String CONTAINERS_URL = API_URL + "/containers";
|
||||
|
||||
private static final String VOLUMES_URL = API_URL + "/volumes";
|
||||
|
||||
@Mock
|
||||
private HttpClientHttp httpClient;
|
||||
|
||||
private DockerApi dockerApi;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.dockerApi = new DockerApi(this.httpClient);
|
||||
}
|
||||
|
||||
private HttpClientHttp httpClient() {
|
||||
return this.httpClient;
|
||||
}
|
||||
|
||||
private Response emptyResponse() throws IOException {
|
||||
return responseOf(null);
|
||||
}
|
||||
|
||||
private Response responseOf(String name) throws IOException {
|
||||
return new Response() {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getContent() throws IOException {
|
||||
if (name == null) {
|
||||
return null;
|
||||
}
|
||||
return getClass().getResourceAsStream(name);
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ImageDockerApiTests {
|
||||
|
||||
private ImageApi api;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<PullImageUpdateEvent> pullListener;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<LoadImageUpdateEvent> loadListener;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<IOConsumer<OutputStream>> writer;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.api = DockerApiTests.this.dockerApi.image();
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullWhenReferenceIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(null, this.pullListener))
|
||||
.withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullWhenListenerIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.pull(ImageReference.of("ubuntu"), null))
|
||||
.withMessage("Listener must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pullPullsImageAndProducesEvents() throws Exception {
|
||||
ImageReference reference = ImageReference.of("cloudfoundry/cnb:bionic");
|
||||
URI createUri = new URI(IMAGES_URL + "/create?fromImage=docker.io%2Fcloudfoundry%2Fcnb%3Abionic");
|
||||
String imageHash = "4acb6bfd6c4f0cabaf7f3690e444afe51f1c7de54d51da7e63fac709c56f1c30";
|
||||
URI imageUri = new URI(IMAGES_URL + "/docker.io/cloudfoundry/cnb@sha256:" + imageHash + "/json");
|
||||
given(httpClient().post(createUri)).willReturn(responseOf("pull-stream.json"));
|
||||
given(httpClient().get(imageUri)).willReturn(responseOf("type/image.json"));
|
||||
Image image = this.api.pull(reference, this.pullListener);
|
||||
assertThat(image.getLayers()).hasSize(46);
|
||||
InOrder ordered = inOrder(this.pullListener);
|
||||
ordered.verify(this.pullListener).onStart();
|
||||
ordered.verify(this.pullListener, times(595)).onUpdate(any());
|
||||
ordered.verify(this.pullListener).onFinish();
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenArchiveIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(null, UpdateListener.none()))
|
||||
.withMessage("Archive must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadWhenListenerIsNullThrowsException() {
|
||||
ImageArchive archive = mock(ImageArchive.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.load(archive, null))
|
||||
.withMessage("Listener must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void loadLoadsImage() throws Exception {
|
||||
Image image = Image.of(getClass().getResourceAsStream("type/image.json"));
|
||||
ImageArchive archive = ImageArchive.from(image);
|
||||
URI loadUri = new URI(IMAGES_URL + "/load");
|
||||
given(httpClient().post(eq(loadUri), eq("application/x-tar"), any()))
|
||||
.willReturn(responseOf("load-stream.json"));
|
||||
this.api.load(archive, this.loadListener);
|
||||
InOrder ordered = inOrder(this.loadListener);
|
||||
ordered.verify(this.loadListener).onStart();
|
||||
ordered.verify(this.loadListener).onUpdate(any());
|
||||
ordered.verify(this.loadListener).onFinish();
|
||||
verify(httpClient()).post(any(), any(), this.writer.capture());
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(21000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeWhenReferenceIsNulllThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true))
|
||||
.withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRemovesContainer() throws Exception {
|
||||
ImageReference reference = ImageReference
|
||||
.of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
|
||||
URI removeUri = new URI(IMAGES_URL
|
||||
+ "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.remove(reference, false);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeWhenForceIsTrueRemovesContainer() throws Exception {
|
||||
ImageReference reference = ImageReference
|
||||
.of("ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d");
|
||||
URI removeUri = new URI(IMAGES_URL
|
||||
+ "/docker.io/library/ubuntu@sha256:6e9f67fa63b0323e9a1e587fd71c561ba48a034504fb804fd26fd8800039835d?force=1");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.remove(reference, true);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class ContainerDockerApiTests {
|
||||
|
||||
private ContainerApi api;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<IOConsumer<OutputStream>> writer;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<LogUpdateEvent> logListener;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.api = DockerApiTests.this.dockerApi.container();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenConfigIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.create(null))
|
||||
.withMessage("Config must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCreatesContainer() throws Exception {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
URI createUri = new URI(CONTAINERS_URL + "/create");
|
||||
given(httpClient().post(eq(createUri), eq("application/json"), any()))
|
||||
.willReturn(responseOf("create-container-response.json"));
|
||||
ContainerReference containerReference = this.api.create(config);
|
||||
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
verify(httpClient()).post(any(), any(), this.writer.capture());
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenHasContentContainerWithContent() throws Exception {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig config = ContainerConfig.of(imageReference, (update) -> update.withCommand("/bin/bash"));
|
||||
TarArchive archive = TarArchive.of((layout) -> {
|
||||
layout.folder("/test", Owner.ROOT);
|
||||
layout.file("/test/file", Owner.ROOT, Content.of("test"));
|
||||
});
|
||||
ContainerContent content = ContainerContent.of(archive);
|
||||
URI createUri = new URI(CONTAINERS_URL + "/create");
|
||||
given(httpClient().post(eq(createUri), eq("application/json"), any()))
|
||||
.willReturn(responseOf("create-container-response.json"));
|
||||
URI uploadUri = new URI(CONTAINERS_URL + "/e90e34656806/archive?path=%2F");
|
||||
given(httpClient().put(eq(uploadUri), eq("application/x-tar"), any())).willReturn(emptyResponse());
|
||||
ContainerReference containerReference = this.api.create(config, content);
|
||||
assertThat(containerReference.toString()).isEqualTo("e90e34656806");
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
verify(httpClient()).post(any(), any(), this.writer.capture());
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(130);
|
||||
verify(httpClient()).put(any(), any(), this.writer.capture());
|
||||
this.writer.getValue().accept(out);
|
||||
assertThat(out.toByteArray()).hasSizeGreaterThan(2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void startWhenReferenceIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.start(null))
|
||||
.withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void startStartsContainer() throws Exception {
|
||||
ContainerReference reference = ContainerReference.of("e90e34656806");
|
||||
URI startContainerUri = new URI(CONTAINERS_URL + "/e90e34656806/start");
|
||||
given(httpClient().post(startContainerUri)).willReturn(emptyResponse());
|
||||
this.api.start(reference);
|
||||
verify(httpClient()).post(startContainerUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
void logsWhenReferenceIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.logs(null, UpdateListener.none()))
|
||||
.withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void logsWhenListenerIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> this.api.logs(ContainerReference.of("e90e34656806"), null))
|
||||
.withMessage("Listener must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void logsProducesEvents() throws Exception {
|
||||
ContainerReference reference = ContainerReference.of("e90e34656806");
|
||||
URI logsUri = new URI(CONTAINERS_URL + "/e90e34656806/logs?stdout=1&stderr=1&follow=1");
|
||||
given(httpClient().get(logsUri)).willReturn(responseOf("log-update-event.stream"));
|
||||
this.api.logs(reference, this.logListener);
|
||||
InOrder ordered = inOrder(this.logListener);
|
||||
ordered.verify(this.logListener).onStart();
|
||||
ordered.verify(this.logListener, times(7)).onUpdate(any());
|
||||
ordered.verify(this.logListener).onFinish();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeWhenReferenceIsNulllThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.remove(null, true))
|
||||
.withMessage("Reference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeRemovesContainer() throws Exception {
|
||||
ContainerReference reference = ContainerReference.of("e90e34656806");
|
||||
URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.remove(reference, false);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeWhenForceIsTrueRemovesContainer() throws Exception {
|
||||
ContainerReference reference = ContainerReference.of("e90e34656806");
|
||||
URI removeUri = new URI(CONTAINERS_URL + "/e90e34656806?force=1");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.remove(reference, true);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class VolumeDockerApiTests {
|
||||
|
||||
private VolumeApi api;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<IOConsumer<OutputStream>> writer;
|
||||
|
||||
@Mock
|
||||
private UpdateListener<LogUpdateEvent> logListener;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
this.api = DockerApiTests.this.dockerApi.volume();
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenNameIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> this.api.delete(null, false))
|
||||
.withMessage("Name must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteDeletesContainer() throws Exception {
|
||||
VolumeName name = VolumeName.of("test");
|
||||
URI removeUri = new URI(VOLUMES_URL + "/test");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.delete(name, false);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteWhenForceIsTrueDeletesContainer() throws Exception {
|
||||
VolumeName name = VolumeName.of("test");
|
||||
URI removeUri = new URI(VOLUMES_URL + "/test?force=1");
|
||||
given(httpClient().delete(removeUri)).willReturn(emptyResponse());
|
||||
this.api.delete(name, true);
|
||||
verify(httpClient()).delete(removeUri);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.util.Collections;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link DockerException}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class DockerExceptionTests {
|
||||
|
||||
private static final URI URI;
|
||||
static {
|
||||
try {
|
||||
URI = new URI("docker://localhost");
|
||||
}
|
||||
catch (URISyntaxException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static final Errors NO_ERRORS = new Errors(Collections.emptyList());
|
||||
|
||||
private static final Errors ERRORS = new Errors(Collections.singletonList(new Errors.Error("code", "message")));
|
||||
|
||||
@Test
|
||||
void createWhenUriIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new DockerException(null, 404, null, NO_ERRORS))
|
||||
.withMessage("URI must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", ERRORS);
|
||||
assertThat(exception.getMessage()).isEqualTo(
|
||||
"Docker API call to 'docker://localhost' failed with status code 404 \"missing\" [code: message]");
|
||||
assertThat(exception.getStatusCode()).isEqualTo(404);
|
||||
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
|
||||
assertThat(exception.getErrors()).isSameAs(ERRORS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenReasonPhraseIsNull() {
|
||||
DockerException exception = new DockerException(URI, 404, null, ERRORS);
|
||||
assertThat(exception.getMessage())
|
||||
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 [code: message]");
|
||||
assertThat(exception.getStatusCode()).isEqualTo(404);
|
||||
assertThat(exception.getReasonPhrase()).isNull();
|
||||
assertThat(exception.getErrors()).isSameAs(ERRORS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenErrorsIsNull() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", null);
|
||||
assertThat(exception.getErrors()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenErrorsIsEmpty() {
|
||||
DockerException exception = new DockerException(URI, 404, "missing", NO_ERRORS);
|
||||
assertThat(exception.getMessage())
|
||||
.isEqualTo("Docker API call to 'docker://localhost' failed with status code 404 \"missing\"");
|
||||
assertThat(exception.getStatusCode()).isEqualTo(404);
|
||||
assertThat(exception.getReasonPhrase()).isEqualTo("missing");
|
||||
assertThat(exception.getErrors()).isSameAs(NO_ERRORS);
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.util.Iterator;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.Errors.Error;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link Errors}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ErrorsTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void readValueDeserializesJson() throws Exception {
|
||||
Errors errors = this.getObjectMapper().readValue(getContent("errors.json"), Errors.class);
|
||||
Iterator<Error> iterator = errors.iterator();
|
||||
Error error1 = iterator.next();
|
||||
Error error2 = iterator.next();
|
||||
assertThat(iterator.hasNext()).isFalse();
|
||||
assertThat(error1.getCode()).isEqualTo("TEST1");
|
||||
assertThat(error1.getMessage()).isEqualTo("Test One");
|
||||
assertThat(error2.getCode()).isEqualTo("TEST2");
|
||||
assertThat(error2.getMessage()).isEqualTo("Test Two");
|
||||
}
|
||||
|
||||
@Test
|
||||
void toStringHasErrorDetails() throws Exception {
|
||||
Errors errors = this.getObjectMapper().readValue(getContent("errors.json"), Errors.class);
|
||||
assertThat(errors.toString()).isEqualTo("[TEST1: Test One, TEST2: Test Two]");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpEntityEnclosingRequest;
|
||||
import org.apache.http.HttpHeaders;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.ClientProtocolException;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpDelete;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.client.methods.HttpPost;
|
||||
import org.apache.http.client.methods.HttpPut;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.Http.Response;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpClientHttp}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class HttpClientHttpTests {
|
||||
|
||||
private static final String APPLICATION_JSON = "application/json";
|
||||
|
||||
@Mock
|
||||
private CloseableHttpClient client;
|
||||
|
||||
@Mock
|
||||
private CloseableHttpResponse response;
|
||||
|
||||
@Mock
|
||||
private StatusLine statusLine;
|
||||
|
||||
@Mock
|
||||
private HttpEntity entity;
|
||||
|
||||
@Mock
|
||||
private InputStream content;
|
||||
|
||||
@Captor
|
||||
private ArgumentCaptor<HttpUriRequest> requestCaptor;
|
||||
|
||||
private HttpClientHttp http;
|
||||
|
||||
private URI uri;
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws Exception {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
given(this.client.execute(any())).willReturn(this.response);
|
||||
given(this.response.getEntity()).willReturn(this.entity);
|
||||
given(this.response.getStatusLine()).willReturn(this.statusLine);
|
||||
this.http = new HttpClientHttp(this.client);
|
||||
this.uri = new URI("docker://localhost/example");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getShouldExecuteHttpGet() throws Exception {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.get(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpGet.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postShouldExecuteHttpPost() throws Exception {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postWithContentShouldExecuteHttpPost() throws Exception {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.post(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPost.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent());
|
||||
assertThat(writeToString(entity)).isEqualTo("test");
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void putWithContentShouldExecuteHttpPut() throws Exception {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.put(this.uri, APPLICATION_JSON,
|
||||
(out) -> StreamUtils.copy("test", StandardCharsets.UTF_8, out));
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
|
||||
assertThat(request).isInstanceOf(HttpPut.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE).getValue()).isEqualTo(APPLICATION_JSON);
|
||||
assertThat(entity.isRepeatable()).isFalse();
|
||||
assertThat(entity.getContentLength()).isEqualTo(-1);
|
||||
assertThat(entity.isStreaming()).isTrue();
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> entity.getContent());
|
||||
assertThat(writeToString(entity)).isEqualTo("test");
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deleteShouldExecuteHttpDelete() throws IOException {
|
||||
given(this.entity.getContent()).willReturn(this.content);
|
||||
given(this.statusLine.getStatusCode()).willReturn(200);
|
||||
Response response = this.http.delete(this.uri);
|
||||
verify(this.client).execute(this.requestCaptor.capture());
|
||||
HttpUriRequest request = this.requestCaptor.getValue();
|
||||
assertThat(request).isInstanceOf(HttpDelete.class);
|
||||
assertThat(request.getURI()).isEqualTo(this.uri);
|
||||
assertThat(request.getFirstHeader(HttpHeaders.CONTENT_TYPE)).isNull();
|
||||
assertThat(response.getContent()).isSameAs(this.content);
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenResposeIsIn400RangeShouldThrowDockerException() throws ClientProtocolException, IOException {
|
||||
given(this.entity.getContent()).willReturn(getClass().getResourceAsStream("errors.json"));
|
||||
given(this.statusLine.getStatusCode()).willReturn(404);
|
||||
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
|
||||
.satisfies((ex) -> assertThat(ex.getErrors()).hasSize(2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void executeWhenResposeIsIn500RangeShouldThrowDockerException() throws ClientProtocolException, IOException {
|
||||
given(this.statusLine.getStatusCode()).willReturn(500);
|
||||
assertThatExceptionOfType(DockerException.class).isThrownBy(() -> this.http.get(this.uri))
|
||||
.satisfies((ex) -> assertThat(ex.getErrors()).isNull());
|
||||
}
|
||||
|
||||
private String writeToString(HttpEntity entity) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
entity.writeTo(out);
|
||||
return new String(out.toByteArray(), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link LoadImageUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LoadImageUpdateEventTests extends ProgressUpdateEventTests {
|
||||
|
||||
@Test
|
||||
void getStreamReturnsStream() {
|
||||
LoadImageUpdateEvent event = (LoadImageUpdateEvent) createEvent();
|
||||
assertThat(event.getStream()).isEqualTo("stream");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||
return new LoadImageUpdateEvent("stream", status, progressDetail, progress);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link LogUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class LogUpdateEventTests {
|
||||
|
||||
@Test
|
||||
void readAllWhenSimpleStreamReturnsEvents() throws Exception {
|
||||
List<LogUpdateEvent> events = readAll("log-update-event.stream");
|
||||
assertThat(events).hasSize(7);
|
||||
assertThat(events.get(0).toString())
|
||||
.isEqualTo("Analyzing image '307c032c4ceaa6330b6c02af945a1fe56a8c3c27c28268574b217c1d38b093cf'");
|
||||
assertThat(events.get(1).toString())
|
||||
.isEqualTo("Writing metadata for uncached layer 'org.cloudfoundry.openjdk:openjdk-jre'");
|
||||
assertThat(events.get(2).toString())
|
||||
.isEqualTo("Using cached launch layer 'org.cloudfoundry.jvmapplication:executable-jar'");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAllWhenAnsiStreamReturnsEvents() throws Exception {
|
||||
List<LogUpdateEvent> events = readAll("log-update-event-ansi.stream");
|
||||
assertThat(events).hasSize(20);
|
||||
assertThat(events.get(0).toString()).isEqualTo("");
|
||||
assertThat(events.get(1).toString()).isEqualTo("Cloud Foundry OpenJDK Buildpack v1.0.64");
|
||||
assertThat(events.get(2).toString()).isEqualTo(" OpenJDK JRE 11.0.5: Reusing cached layer");
|
||||
}
|
||||
|
||||
private List<LogUpdateEvent> readAll(String name) throws IOException {
|
||||
List<LogUpdateEvent> events = new ArrayList<>();
|
||||
try (InputStream inputStream = getClass().getResourceAsStream(name)) {
|
||||
LogUpdateEvent.readAll(inputStream, events::add);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ProgressUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
abstract class ProgressUpdateEventTests {
|
||||
|
||||
@Test
|
||||
void getStatusReturnsStatus() {
|
||||
ProgressUpdateEvent event = createEvent();
|
||||
assertThat(event.getStatus()).isEqualTo("status");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getProgressDetailsReturnsProgresssDetails() {
|
||||
ProgressUpdateEvent event = createEvent();
|
||||
assertThat(event.getProgressDetail().getCurrent()).isEqualTo(1);
|
||||
assertThat(event.getProgressDetail().getTotal()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getProgressReturnsProgress() {
|
||||
ProgressUpdateEvent event = createEvent();
|
||||
assertThat(event.getProgress()).isEqualTo("progress");
|
||||
}
|
||||
|
||||
@Test
|
||||
void progressDetailIsEmptyWhenCurrentIsNullReturnsTrue() {
|
||||
ProgressDetail detail = new ProgressDetail(null, 2);
|
||||
assertThat(ProgressDetail.isEmpty(detail)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void progressDetailIsEmptyWhenTotalIsNullReturnsTrue() {
|
||||
ProgressDetail detail = new ProgressDetail(1, null);
|
||||
assertThat(ProgressDetail.isEmpty(detail)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void progressDetailIsEmptyWhenTotalAndCurrentAreNotNullReturnsFalse() {
|
||||
ProgressDetail detail = new ProgressDetail(1, 2);
|
||||
assertThat(ProgressDetail.isEmpty(detail)).isFalse();
|
||||
}
|
||||
|
||||
protected ProgressUpdateEvent createEvent() {
|
||||
return createEvent("status", new ProgressDetail(1, 2), "progress");
|
||||
}
|
||||
|
||||
protected abstract ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress);
|
||||
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.docker.ProgressUpdateEvent.ProgressDetail;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PullImageUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PullImageUpdateEventTests extends ProgressUpdateEventTests {
|
||||
|
||||
@Test
|
||||
void getIdReturnsId() {
|
||||
PullImageUpdateEvent event = (PullImageUpdateEvent) createEvent();
|
||||
assertThat(event.getId()).isEqualTo("id");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ProgressUpdateEvent createEvent(String status, ProgressDetail progressDetail, String progress) {
|
||||
return new PullImageUpdateEvent("id", status, progressDetail, progress);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link PullImageUpdateEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class PullUpdateEventTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void readValueWhenFullDeserializesJson() throws Exception {
|
||||
PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-full.json"),
|
||||
PullImageUpdateEvent.class);
|
||||
assertThat(event.getId()).isEqualTo("4f4fb700ef54");
|
||||
assertThat(event.getStatus()).isEqualTo("Extracting");
|
||||
assertThat(event.getProgressDetail().getCurrent()).isEqualTo(16);
|
||||
assertThat(event.getProgressDetail().getTotal()).isEqualTo(32);
|
||||
assertThat(event.getProgress()).isEqualTo("[==================================================>] 32B/32B");
|
||||
}
|
||||
|
||||
@Test
|
||||
void readValueWhenMinimalDeserializesJson() throws Exception {
|
||||
PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-update-minimal.json"),
|
||||
PullImageUpdateEvent.class);
|
||||
assertThat(event.getId()).isNull();
|
||||
assertThat(event.getStatus()).isEqualTo("Status: Downloaded newer image for cloudfoundry/cnb:bionic");
|
||||
assertThat(event.getProgressDetail()).isNull();
|
||||
assertThat(event.getProgress()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void readValueWhenEmptyDetailsDeserializesJson() throws Exception {
|
||||
PullImageUpdateEvent event = getObjectMapper().readValue(getContent("pull-with-empty-details.json"),
|
||||
PullImageUpdateEvent.class);
|
||||
assertThat(event.getId()).isEqualTo("d837a2a1365e");
|
||||
assertThat(event.getStatus()).isEqualTo("Pulling fs layer");
|
||||
assertThat(event.getProgressDetail()).isNull();
|
||||
assertThat(event.getProgress()).isNull();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link TotalProgressBar}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TotalProgressBarTests {
|
||||
|
||||
@Test
|
||||
void withPrefixAndBookends() {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
TotalProgressBar bar = new TotalProgressBar("prefix:", '#', true, out);
|
||||
assertThat(out).hasToString("prefix: [ ");
|
||||
bar.accept(new TotalProgressEvent(10));
|
||||
assertThat(out.toString()).isEqualTo("prefix: [ #####");
|
||||
bar.accept(new TotalProgressEvent(50));
|
||||
assertThat(out.toString()).isEqualTo("prefix: [ #########################");
|
||||
bar.accept(new TotalProgressEvent(100));
|
||||
assertThat(out.toString()).isEqualTo("prefix: [ ################################################## ]\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutPrefix() {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
TotalProgressBar bar = new TotalProgressBar(null, '#', true, out);
|
||||
assertThat(out).hasToString("[ ");
|
||||
bar.accept(new TotalProgressEvent(10));
|
||||
assertThat(out.toString()).isEqualTo("[ #####");
|
||||
bar.accept(new TotalProgressEvent(50));
|
||||
assertThat(out.toString()).isEqualTo("[ #########################");
|
||||
bar.accept(new TotalProgressEvent(100));
|
||||
assertThat(out.toString()).isEqualTo("[ ################################################## ]\n");
|
||||
}
|
||||
|
||||
@Test
|
||||
void withoutBookends() {
|
||||
TestPrintStream out = new TestPrintStream();
|
||||
TotalProgressBar bar = new TotalProgressBar("", '.', false, out);
|
||||
assertThat(out).hasToString("");
|
||||
bar.accept(new TotalProgressEvent(10));
|
||||
assertThat(out.toString()).isEqualTo(".....");
|
||||
bar.accept(new TotalProgressEvent(50));
|
||||
assertThat(out.toString()).isEqualTo(".........................");
|
||||
bar.accept(new TotalProgressEvent(100));
|
||||
assertThat(out.toString()).isEqualTo("..................................................\n");
|
||||
}
|
||||
|
||||
static class TestPrintStream extends PrintStream {
|
||||
|
||||
TestPrintStream() {
|
||||
super(new ByteArrayOutputStream());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.out.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link TotalProgressEvent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TotalProgressEventTests {
|
||||
|
||||
@Test
|
||||
void create() {
|
||||
assertThat(new TotalProgressEvent(0).getPercent()).isEqualTo(0);
|
||||
assertThat(new TotalProgressEvent(10).getPercent()).isEqualTo(10);
|
||||
assertThat(new TotalProgressEvent(100).getPercent()).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenPercentLessThanZeroThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(-1))
|
||||
.withMessage("Percent must be in the range 0 to 100");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenEventMoreThanOneHundredThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new TotalProgressEvent(101))
|
||||
.withMessage("Percent must be in the range 0 to 100");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
import org.springframework.boot.cloudnativebuildpack.json.JsonStream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link TotalProgressPullListener}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class TotalProgressPullListenerTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void totalProgress() throws Exception {
|
||||
List<Integer> progress = new ArrayList<>();
|
||||
TotalProgressPullListener listener = new TotalProgressPullListener((event) -> progress.add(event.getPercent()));
|
||||
run(listener);
|
||||
int last = 0;
|
||||
for (Integer update : progress) {
|
||||
assertThat(update).isGreaterThanOrEqualTo(last);
|
||||
last = update;
|
||||
}
|
||||
assertThat(last).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled("For visual inspection")
|
||||
void totalProgressUpdatesSmoothly() throws Exception {
|
||||
TestTotalProgressPullListener listener = new TestTotalProgressPullListener(
|
||||
new TotalProgressBar("Pulling layers:"));
|
||||
run(listener);
|
||||
}
|
||||
|
||||
private void run(TotalProgressPullListener listener) throws IOException {
|
||||
JsonStream jsonStream = new JsonStream(getObjectMapper());
|
||||
listener.onStart();
|
||||
jsonStream.get(getContent("pull-stream.json"), PullImageUpdateEvent.class, listener::onUpdate);
|
||||
listener.onFinish();
|
||||
}
|
||||
|
||||
private static class TestTotalProgressPullListener extends TotalProgressPullListener {
|
||||
|
||||
TestTotalProgressPullListener(Consumer<TotalProgressEvent> consumer) {
|
||||
super(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpdate(PullImageUpdateEvent event) {
|
||||
super.onUpdate(event);
|
||||
try {
|
||||
Thread.sleep(10);
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.json.AbstractJsonTests;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
/**
|
||||
* Tests for {@link ContainerConfig}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ContainerConfigTests extends AbstractJsonTests {
|
||||
|
||||
@Test
|
||||
void ofWhenImageReferenceIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(null, (update) -> {
|
||||
})).withMessage("ImageReference must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenUpdateIsNullThrowsException() {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ContainerConfig.of(imageReference, null))
|
||||
.withMessage("Update must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeToWritesJson() throws Exception {
|
||||
ImageReference imageReference = ImageReference.of("ubuntu:bionic");
|
||||
ContainerConfig containerConfig = ContainerConfig.of(imageReference, (update) -> {
|
||||
update.withUser("root");
|
||||
update.withCommand("ls", "-l");
|
||||
update.withArgs("-h");
|
||||
update.withLabel("spring", "boot");
|
||||
update.withBind("bind-source", "bind-dest");
|
||||
});
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
containerConfig.writeTo(outputStream);
|
||||
String actualJson = new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
|
||||
String expectedJson = StreamUtils.copyToString(getContent("container-config.json"), StandardCharsets.UTF_8);
|
||||
JSONAssert.assertEquals(expectedJson, actualJson, false);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.boot.cloudnativebuildpack.docker.type;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.cloudnativebuildpack.io.TarArchive;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link ContainerContent}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ContainerContentTests {
|
||||
|
||||
@Test
|
||||
void ofWhenArchiveIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(null))
|
||||
.withMessage("Archive must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenDestinationPathIsNullThrowsException() {
|
||||
TarArchive archive = mock(TarArchive.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, null))
|
||||
.withMessage("DestinationPath must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWhenDestinationPathIsEmptyThrowsException() {
|
||||
TarArchive archive = mock(TarArchive.class);
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> ContainerContent.of(archive, ""))
|
||||
.withMessage("DestinationPath must not be empty");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofCreatesContainerContent() {
|
||||
TarArchive archive = mock(TarArchive.class);
|
||||
ContainerContent content = ContainerContent.of(archive);
|
||||
assertThat(content.getArchive()).isSameAs(archive);
|
||||
assertThat(content.getDestinationPath()).isEqualTo("/");
|
||||
}
|
||||
|
||||
@Test
|
||||
void ofWithDestinationPathCreatesContainerContent() {
|
||||
TarArchive archive = mock(TarArchive.class);
|
||||
ContainerContent content = ContainerContent.of(archive, "/test");
|
||||
assertThat(content.getArchive()).isSameAs(archive);
|
||||
assertThat(content.getDestinationPath()).isEqualTo("/test");
|
||||
}
|
||||
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue