Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* @author Phillip Webb
* @author Scott Frederick
* @author Andrey Shlykov
* @author Rafael Ceccone
* @since 2.3.0
*/
public abstract class AbstractBuildLog implements BuildLog {
Expand Down Expand Up @@ -89,6 +90,12 @@ public void executedLifecycle(BuildRequest request) {
log();
}

@Override
public void createdTag(ImageReference tag) {
log("Successfully created image tag '" + tag + "'");
log();
}

private String getDigest(Image image) {
List<String> digests = image.getDigests();
return (digests.isEmpty() ? "" : digests.get(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* @author Phillip Webb
* @author Scott Frederick
* @author Andrey Shlykov
* @author Rafael Ceccone
* @since 2.3.0
* @see #toSystemOut()
*/
Expand Down Expand Up @@ -99,6 +100,12 @@ public interface BuildLog {
*/
void executedLifecycle(BuildRequest request);

/**
* Log that a tag has been created.
* @param tag the tag reference
*/
void createdTag(ImageReference tag);

/**
* Factory method that returns a {@link BuildLog} the outputs to {@link System#out}.
* @return a build log instance that logs to system out
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
* @author Phillip Webb
* @author Scott Frederick
* @author Andrey Shlykov
* @author Rafael Ceccone
* @since 2.3.0
*/
public class BuildRequest {
Expand Down Expand Up @@ -68,6 +69,8 @@ public class BuildRequest {

private final List<Binding> bindings;

private final List<ImageReference> tags;

BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(applicationContent, "ApplicationContent must not be null");
Expand All @@ -83,12 +86,13 @@ public class BuildRequest {
this.creator = Creator.withVersion("");
this.buildpacks = Collections.emptyList();
this.bindings = Collections.emptyList();
this.tags = Collections.emptyList();
}

BuildRequest(ImageReference name, Function<Owner, TarArchive> applicationContent, ImageReference builder,
ImageReference runImage, Creator creator, Map<String, String> env, boolean cleanCache,
boolean verboseLogging, PullPolicy pullPolicy, boolean publish, List<BuildpackReference> buildpacks,
List<Binding> bindings) {
List<Binding> bindings, List<ImageReference> tags) {
this.name = name;
this.applicationContent = applicationContent;
this.builder = builder;
Expand All @@ -101,6 +105,7 @@ public class BuildRequest {
this.publish = publish;
this.buildpacks = buildpacks;
this.bindings = bindings;
this.tags = tags;
}

/**
Expand All @@ -112,7 +117,7 @@ public BuildRequest withBuilder(ImageReference builder) {
Assert.notNull(builder, "Builder must not be null");
return new BuildRequest(this.name, this.applicationContent, builder.inTaggedOrDigestForm(), this.runImage,
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings);
this.buildpacks, this.bindings, this.tags);
}

/**
Expand All @@ -123,7 +128,7 @@ public BuildRequest withBuilder(ImageReference builder) {
public BuildRequest withRunImage(ImageReference runImageName) {
return new BuildRequest(this.name, this.applicationContent, this.builder, runImageName.inTaggedOrDigestForm(),
this.creator, this.env, this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings);
this.buildpacks, this.bindings, this.tags);
}

/**
Expand All @@ -134,7 +139,8 @@ public BuildRequest withRunImage(ImageReference runImageName) {
public BuildRequest withCreator(Creator creator) {
Assert.notNull(creator, "Creator must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -150,7 +156,7 @@ public BuildRequest withEnv(String name, String value) {
env.put(name, value);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(env), this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish,
this.buildpacks, this.bindings);
this.buildpacks, this.bindings, this.tags);
}

/**
Expand All @@ -164,7 +170,7 @@ public BuildRequest withEnv(Map<String, String> env) {
updatedEnv.putAll(env);
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator,
Collections.unmodifiableMap(updatedEnv), this.cleanCache, this.verboseLogging, this.pullPolicy,
this.publish, this.buildpacks, this.bindings);
this.publish, this.buildpacks, this.bindings, this.tags);
}

/**
Expand All @@ -174,7 +180,8 @@ public BuildRequest withEnv(Map<String, String> env) {
*/
public BuildRequest withCleanCache(boolean cleanCache) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -184,7 +191,8 @@ public BuildRequest withCleanCache(boolean cleanCache) {
*/
public BuildRequest withVerboseLogging(boolean verboseLogging) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings);
this.cleanCache, verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -194,7 +202,8 @@ public BuildRequest withVerboseLogging(boolean verboseLogging) {
*/
public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings);
this.cleanCache, this.verboseLogging, pullPolicy, this.publish, this.buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -204,7 +213,8 @@ public BuildRequest withPullPolicy(PullPolicy pullPolicy) {
*/
public BuildRequest withPublish(boolean publish) {
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, publish, this.buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -227,7 +237,8 @@ public BuildRequest withBuildpacks(BuildpackReference... buildpacks) {
public BuildRequest withBuildpacks(List<BuildpackReference> buildpacks) {
Assert.notNull(buildpacks, "Buildpacks must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, buildpacks, this.bindings,
this.tags);
}

/**
Expand All @@ -250,7 +261,30 @@ public BuildRequest withBindings(Binding... bindings) {
public BuildRequest withBindings(List<Binding> bindings) {
Assert.notNull(bindings, "Bindings must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings);
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, bindings,
this.tags);
}

/**
* Return a new {@link BuildRequest} with updated tags.
* @param tags a collection of tags to be created for the built image
* @return an updated build request
*/
public BuildRequest withTags(ImageReference... tags) {
Assert.notEmpty(tags, "Tags must not be empty");
return withTags(Arrays.asList(tags));
}

/**
* Return a new {@link BuildRequest} with updated tags.
* @param tags a collection of tags to be created for the built image
* @return an updated build request
*/
public BuildRequest withTags(List<ImageReference> tags) {
Assert.notNull(tags, "Tags must not be null");
return new BuildRequest(this.name, this.applicationContent, this.builder, this.runImage, this.creator, this.env,
this.cleanCache, this.verboseLogging, this.pullPolicy, this.publish, this.buildpacks, this.bindings,
tags);
}

/**
Expand Down Expand Up @@ -353,6 +387,14 @@ public List<Binding> getBindings() {
return this.bindings;
}

/**
* Return the collection of tags that should be created.
* @return the tags
*/
public List<ImageReference> getTags() {
return this.tags;
}

/**
* Factory method to create a new {@link BuildRequest} from a JAR file.
* @param jarFile the source jar file
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
* @author Phillip Webb
* @author Scott Frederick
* @author Andrey Shlykov
* @author Rafael Ceccone
* @since 2.3.0
*/
public class Builder {
Expand Down Expand Up @@ -110,8 +111,10 @@ public void build(BuildRequest request) throws DockerEngineException, IOExceptio
this.docker.image().load(ephemeralBuilder.getArchive(), UpdateListener.none());
try {
executeLifecycle(request, ephemeralBuilder);
createTags(request.getName(), request.getTags());
if (request.isPublish()) {
pushImage(request.getName());
pushTags(request.getTags());
}
}
finally {
Expand Down Expand Up @@ -157,6 +160,19 @@ private void pushImage(ImageReference reference) throws IOException {
this.log.pushedImage(reference);
}

private void createTags(ImageReference sourceReference, List<ImageReference> tags) throws IOException {
for (ImageReference tag : tags) {
this.docker.image().tag(sourceReference, tag);
this.log.createdTag(tag);
}
}

private void pushTags(List<ImageReference> tags) throws IOException {
for (ImageReference tag : tags) {
pushImage(tag);
}
}

private String getBuilderAuthHeader() {
return (this.dockerConfiguration != null && this.dockerConfiguration.getBuilderRegistryAuthentication() != null)
? this.dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader() : null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
*
* @author Phillip Webb
* @author Scott Frederick
* @author Rafael Ceccone
* @since 2.3.0
*/
public class DockerApi {
Expand Down Expand Up @@ -300,6 +301,13 @@ public Image inspect(ImageReference reference) throws IOException {
}
}

public void tag(ImageReference sourceReference, ImageReference targetReference) throws IOException {
Assert.notNull(sourceReference, "SourceReference must not be null");
Assert.notNull(targetReference, "TargetReference must not be null");
URI uri = buildUrl("/images/" + sourceReference + "/tag", "repo", targetReference.toString());
http().post(uri);
}

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
*
* @author Phillip Webb
* @author Scott Frederick
* @author Rafael Ceccone
*/
public class BuildRequestTests {

Expand Down Expand Up @@ -199,6 +200,25 @@ void withBindingsWhenBindingsIsNullThrowsException() throws IOException {
.withMessage("Bindings must not be null");
}

@Test
void withTagsAddsTags() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
BuildRequest witTags = request.withTags(ImageReference.of("docker.io/library/my-app:latest"),
ImageReference.of("example.com/custom/my-app:0.0.1"),
ImageReference.of("example.com/custom/my-app:latest"));
assertThat(request.getTags()).isEmpty();
assertThat(witTags.getTags()).containsExactly(ImageReference.of("docker.io/library/my-app:latest"),
ImageReference.of("example.com/custom/my-app:0.0.1"),
ImageReference.of("example.com/custom/my-app:latest"));
}

@Test
void withTagsWhenTagsIsNullThrowsException() throws IOException {
BuildRequest request = BuildRequest.forJarFile(writeTestJarFile("my-app-0.0.1.jar"));
assertThatIllegalArgumentException().isThrownBy(() -> request.withTags((List<ImageReference>) null))
.withMessage("Tags must not be null");
}

private void hasExpectedJarContent(TarArchive archive) {
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
*
* @author Phillip Webb
* @author Scott Frederick
* @author Rafael Ceccone
*/
class BuilderTests {

Expand Down Expand Up @@ -276,6 +277,65 @@ void buildInvokesBuilderWithIfNotPresentPullPolicy() throws Exception {
verify(docker.image(), times(2)).pull(any(), any(), isNull());
}

@Test
void buildInvokesBuilderWithTags() 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(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(), isNull()))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(), isNull()))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, null);
BuildRequest request = getTestRequest().withTags(ImageReference.of("my-application:1.2.3"));
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
verify(docker.image()).load(archive.capture(), any());
verify(docker.image()).remove(archive.getValue().getTag(), true);
}

@Test
void buildInvokesBuilderWithTagsAndPublishesImageAndTags() throws Exception {
TestPrintStream out = new TestPrintStream();
DockerApi docker = mockDockerApi();
Image builderImage = loadImage("image.json");
Image runImage = loadImage("run-image.json");
DockerConfiguration dockerConfiguration = new DockerConfiguration()
.withBuilderRegistryTokenAuthentication("builder token")
.withPublishRegistryTokenAuthentication("publish token");
given(docker.image().pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(builderImage));
given(docker.image().pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader())))
.willAnswer(withPulledImage(runImage));
Builder builder = new Builder(BuildLog.to(out), docker, dockerConfiguration);
BuildRequest request = getTestRequest().withPublish(true).withTags(ImageReference.of("my-application:1.2.3"));
builder.build(request);
assertThat(out.toString()).contains("Running creator");
assertThat(out.toString()).contains("Successfully built image 'docker.io/library/my-application:latest'");
assertThat(out.toString()).contains("Successfully created image tag 'docker.io/library/my-application:1.2.3'");

verify(docker.image()).pull(eq(ImageReference.of(BuildRequest.DEFAULT_BUILDER_IMAGE_NAME)), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
verify(docker.image()).pull(eq(ImageReference.of("docker.io/cloudfoundry/run:base-cnb")), any(),
eq(dockerConfiguration.getBuilderRegistryAuthentication().getAuthHeader()));
verify(docker.image()).push(eq(request.getName()), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
verify(docker.image()).tag(eq(request.getName()), eq(ImageReference.of("my-application:1.2.3")));
verify(docker.image()).push(eq(ImageReference.of("my-application:1.2.3")), any(),
eq(dockerConfiguration.getPublishRegistryAuthentication().getAuthHeader()));
ArgumentCaptor<ImageArchive> archive = ArgumentCaptor.forClass(ImageArchive.class);
verify(docker.image()).load(archive.capture(), any());
verify(docker.image()).remove(archive.getValue().getTag(), true);
verifyNoMoreInteractions(docker.image());
}

@Test
void buildWhenStackIdDoesNotMatchThrowsException() throws Exception {
TestPrintStream out = new TestPrintStream();
Expand Down
Loading