Mimimal support for building image with Dockerfile

This commit is contained in:
Kris De Volder
2020-10-29 12:17:47 -07:00
parent 9557cacd88
commit 833950648b
10 changed files with 293 additions and 86 deletions

View File

@@ -0,0 +1,26 @@
/*******************************************************************************
* Copyright (c) 2020 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.docker.exceptions;
import org.springframework.ide.eclipse.boot.dash.api.AppConsole;
public class DockerBuildException extends Exception {
private static final long serialVersionUID = 1L;
public DockerBuildException(String string) {
super(string);
}
public void writeDetailedExplanation(AppConsole console) throws Exception {
}
}

View File

@@ -0,0 +1,71 @@
/*******************************************************************************
* Copyright (c) 2020 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.docker.exceptions;
import java.io.File;
import java.util.List;
import org.springframework.ide.eclipse.boot.dash.api.AppConsole;
import org.springframework.ide.eclipse.boot.dash.console.LogType;
/**
* Exception raised when BootDash wants to build a docker image but can not
* find / determine how to build it. I.e. we are looking for a number of
* different build scripts in project-relative locations. If none of
* them are found then this exception is raised.
*/
public class MissingBuildScriptException extends DockerBuildException {
private static final long serialVersionUID = 1L;
public final List<File> locationsChecked;
public MissingBuildScriptException(List<File> locationsChecked) {
super("Neither maven wrapper, gradle wrapper or custom build script found.");
this.locationsChecked = locationsChecked;
}
@Override
public void writeDetailedExplanation(AppConsole console) throws Exception {
console.write("Places we looked for build script: ", LogType.STDERROR);
for (File loc : this.locationsChecked) {
console.write(" - "+loc, LogType.STDERROR);
}
showBuildScriptHelp(console);
}
private void showBuildScriptHelp(AppConsole console) {
String[] help = {
"To build a docker image, Boot Dash needs to run a build script from your project.",
"Three different types are supported and checked for in this order:",
"",
"1. "+this.locationsChecked.get(0).getAbsoluteFile().getName(),
" A custom script placed by you at the project root.",
" Typically this runs a custom maven or gradle command on your project.",
"",
"2. maven",
" If your project has a mvnw, we will use that to execute the `spring-boot:build-image` task",
"",
"3. gradle",
" If your project has a gradlew, we will use that to execute the `bootBuildImage` task",
};
try {
for (String line : help) {
console.write(line, LogType.STDERROR);
}
} catch (Exception e1) {
//ignore
}
}
}

View File

@@ -0,0 +1,41 @@
/*******************************************************************************
* Copyright (c) 2020 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.docker.exceptions;
import java.util.regex.Pattern;
import org.springframework.ide.eclipse.boot.dash.api.AppConsole;
import org.springframework.ide.eclipse.boot.dash.console.LogType;
public class MissingBuildTagException extends DockerBuildException {
private static final long serialVersionUID = 1L;
public final Pattern[] whatWeLookedFor;
public MissingBuildTagException(Pattern... whatWeLookedFor) {
super("Couldn't detect the image id or tag");
this.whatWeLookedFor = whatWeLookedFor;
}
@Override
public void writeDetailedExplanation(AppConsole console) throws Exception {
console.write("We detect the image tag or hash by matching specific regexp patterns", LogType.STDERROR);
console.write("in the build output. But none of the patterns we look for where found.", LogType.STDERROR);
console.write("", LogType.STDERROR);
console.write("These are the patterns that we looked for:", LogType.STDERROR);
console.write("", LogType.STDERROR);
for (Pattern pattern : whatWeLookedFor) {
console.write(" regexp: /"+pattern+"/", LogType.STDERROR);
}
}
}

View File

@@ -23,7 +23,6 @@ import java.util.Map.Entry;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -49,6 +48,9 @@ import org.springframework.ide.eclipse.boot.dash.api.SystemPropertySupport;
import org.springframework.ide.eclipse.boot.dash.api.TemporalBoolean;
import org.springframework.ide.eclipse.boot.dash.console.LogType;
import org.springframework.ide.eclipse.boot.dash.devtools.DevtoolsUtil;
import org.springframework.ide.eclipse.boot.dash.docker.exceptions.DockerBuildException;
import org.springframework.ide.eclipse.boot.dash.docker.exceptions.MissingBuildScriptException;
import org.springframework.ide.eclipse.boot.dash.docker.exceptions.MissingBuildTagException;
import org.springframework.ide.eclipse.boot.dash.docker.jmx.JmxSupport;
import org.springframework.ide.eclipse.boot.dash.docker.runtarget.BuildScriptLocator.BuildKind;
import org.springframework.ide.eclipse.boot.dash.labels.BootDashLabels;
@@ -70,6 +72,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerCmd;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.InspectImageResponse;
import com.github.dockerjava.api.exception.NotModifiedException;
import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.ExposedPort;
@@ -246,46 +249,17 @@ public class DockerApp extends AbstractDisposable implements App, ChildBearing,
return build(console);
} catch (Exception e) {
console.write(ExceptionUtil.getMessage(e), LogType.STDERROR);
if (e instanceof MissingBuildScriptException) {
console.write("Places we looked for build script: ", LogType.STDERROR);
for (File loc : ((MissingBuildScriptException) e).locationsChecked) {
console.write(" - "+loc, LogType.STDERROR);
}
showBuildScriptHelp(console, (MissingBuildScriptException) e);
if (e instanceof DockerBuildException) {
((DockerBuildException) e).writeDetailedExplanation(console);
}
throw e;
}
});
refreshTracker.run("Starting container '" + image + "'" + + BootDashLabels.ELLIPSIS, () -> {
run(console, image, deployment);
});
}
private void showBuildScriptHelp(AppConsole console, MissingBuildScriptException e) {
String[] help = {
"To build a docker image, Boot Dash needs to run a build script from your project.",
"Three different types are supported and checked for in this order:",
"",
"1. "+e.locationsChecked.get(0).getAbsoluteFile().getName(),
" A custom script placed by you at the project root.",
" Typically this runs a custom maven or gradle command on your project.",
"",
"2. maven",
" If your project has a mvnw, we will use that to execute the `spring-boot:build-image` task",
"",
"3. gradle",
" If your project has a gradlew, we will use that to execute the `bootBuildImage` task",
};
try {
for (String line : help) {
console.write(line, LogType.STDERROR);
}
} catch (Exception e1) {
//ignore
}
}
private void run(AppConsole console, String image, DockerDeployment deployment) throws Exception {
if (client==null) {
console.write("Cannot start container... Docker client is disconnected!", LogType.STDERROR);
@@ -395,11 +369,24 @@ public class DockerApp extends AbstractDisposable implements App, ChildBearing,
containerLogConnection.setValue(DockerContainer.connectLog(target, c.getId(), console, true));
}
}
/* Sample output from `docker build -t fui .`
Successfully built f3157a980fd2
Successfully tagged fui:latest
*/
private static final Pattern BUILT_IMAGE_MESSAGE = Pattern.compile("Successfully built image.*\\'(.*)\\'");
/**
* List of patterns that we look for in order of priority. If more than one pattern matches, the first pattern
* takes in this list takes priority over the next.
*/
private static final Pattern[] BUILT_IMAGE_MESSAGE_PATS = {
Pattern.compile("Successfully built image.*\\'(.*)\\'"), //from mvn spring-boot:build-image
Pattern.compile("Successfully tagged ([^\\s]+)"), //from `docker build -t <name> .`
Pattern.compile("Successfully built ([a-f0-9]+)"), //from `docker build .`
};
private String build(AppConsole console) throws Exception {
AtomicReference<String> image = new AtomicReference<>();
String[] imageIds = new String[BUILT_IMAGE_MESSAGE_PATS.length];
File directory = new File(project.getLocation().toString());
String[] command = getBuildCommand(directory);
@@ -412,9 +399,11 @@ public class DockerApp extends AbstractDisposable implements App, ChildBearing,
Process process = builder.start();
LineBasedStreamGobler outputGobler = new LineBasedStreamGobler(process.getInputStream(), (line) -> {
System.out.println(line);
Matcher matcher = BUILT_IMAGE_MESSAGE.matcher(line);
if (matcher.find()) {
image.set(matcher.group(1));
for (int i = 0; i < BUILT_IMAGE_MESSAGE_PATS.length; i++) {
Matcher matcher = BUILT_IMAGE_MESSAGE_PATS[i].matcher(line);
if (matcher.find()) {
imageIds[i] = matcher.group(1);
}
}
try {
console.write(line, LogType.APP_OUT);
@@ -435,16 +424,29 @@ public class DockerApp extends AbstractDisposable implements App, ChildBearing,
}
outputGobler.join();
String imageTag = image.get();
String imageTag = null;
for (String found : imageIds) {
if (found!=null) {
imageTag = found;
break;
}
}
if (imageTag==null) {
throw new MissingBuildTagException(BUILT_IMAGE_MESSAGE_PATS);
}
if (imageTag.startsWith(DOCKER_IO_LIBRARY)) {
imageTag = imageTag.substring(DOCKER_IO_LIBRARY.length());
}
List<Image> images = client.listImagesCmd().withImageNameFilter(imageTag).exec();
for (Image img : images) {
addPersistedImage(img.getId());
if (images.isEmpty()) {
// maybe the 'imageTag' is not actually a tag but an id/hash.
InspectImageResponse inspect = client.inspectImageCmd(imageTag).exec();
addPersistedImage(inspect.getId());
} else {
for (Image img : images) {
addPersistedImage(img.getId());
}
}
return imageTag;
}

View File

@@ -100,11 +100,16 @@ public class DockerContainer implements App, RunStateProvider, JmxConnectable, S
return Suppliers.memoize(() ->{
Map<String, String> labels = labelsSupplier.get();
try {
Map<?,?> bpmd = new ObjectMapper().readValue(labels.get("io.buildpacks.build.metadata"), Map.class);
Set<String> deps = dependencyNamePath
.traverseAmbiguously(YamlNavigable.javaObject(bpmd))
.flatMap(JavaObjectNav::asStringMaybe).collect(Collectors.toSet());
return deps.contains("spring-boot-devtools");
if (labels!=null) {
String buildpackMetadata = labels.get("io.buildpacks.build.metadata");
if (buildpackMetadata!=null) {
Map<?,?> bpmd = new ObjectMapper().readValue(buildpackMetadata, Map.class);
Set<String> deps = dependencyNamePath
.traverseAmbiguously(YamlNavigable.javaObject(bpmd))
.flatMap(JavaObjectNav::asStringMaybe).collect(Collectors.toSet());
return deps.contains("spring-boot-devtools");
}
}
} catch (Exception e) {
Log.log(e);
}

View File

@@ -62,7 +62,6 @@ public class FakeDockerRunCommand {
bindTo.getHostIp()+":"+bindTo.getHostPortSpec()+":"+exposed.getPort()+"/"+exposed.getProtocol()
);
}
System.out.println(entry);
}
}
}

View File

@@ -1,33 +0,0 @@
/*******************************************************************************
* Copyright (c) 2020 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.dash.docker.runtarget;
import java.io.File;
import java.util.List;
/**
* Exception raised when BootDash wants to build a docker image but can not
* find / determine how to build it. I.e. we are looking for a number of
* different build scripts in project-relative locations. If none of
* them are found then this exception is raised.
*/
public class MissingBuildScriptException extends Exception {
private static final long serialVersionUID = 1L;
public final List<File> locationsChecked;
public MissingBuildScriptException(List<File> locationsChecked) {
super("Niether maven wraper, gradle wrapper or custom build script found.");
this.locationsChecked = locationsChecked;
}
}

View File

@@ -16,6 +16,9 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -26,8 +29,6 @@ import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assert
import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.assertNotContains;
import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.createFile;
import static org.mockito.ArgumentMatchers.*;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
@@ -53,7 +54,6 @@ import org.eclipse.swt.graphics.Color;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
import org.mockito.Matchers;
import org.mockito.Mockito;
import org.springframework.ide.eclipse.beans.ui.live.model.LiveBeansModel;
@@ -77,6 +77,7 @@ import org.springframework.ide.eclipse.boot.dash.model.BootDashElement;
import org.springframework.ide.eclipse.boot.dash.model.BootDashModel;
import org.springframework.ide.eclipse.boot.dash.model.BootProjectDashElement;
import org.springframework.ide.eclipse.boot.dash.model.Failable;
import org.springframework.ide.eclipse.boot.dash.model.RefreshState;
import org.springframework.ide.eclipse.boot.dash.model.RunState;
import org.springframework.ide.eclipse.boot.dash.model.Taggable;
import org.springframework.ide.eclipse.boot.dash.model.actuator.RequestMapping;
@@ -100,15 +101,17 @@ import org.springframework.ide.eclipse.boot.test.BootProjectTestHarness;
import org.springframework.ide.eclipse.boot.test.util.TestBracketter;
import org.springsource.ide.eclipse.commons.core.util.StringUtil;
import org.springsource.ide.eclipse.commons.frameworks.test.util.ACondition;
import org.springsource.ide.eclipse.commons.tests.util.StsTestCase;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.InspectContainerResponse;
import com.github.dockerjava.api.command.InspectImageResponse;
import com.github.dockerjava.api.model.Container;
import com.github.dockerjava.api.model.Image;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import static org.springsource.ide.eclipse.commons.tests.util.StsTestCase.*;
public class BootDashDockerTests {
private static final int BUILD_IMAGE_TIMEOUT = 30_000;
@@ -122,6 +125,90 @@ public class BootDashDockerTests {
createDockerTarget();
}
@Test
public void projectWithdDockerFile() throws Exception {
GenericRemoteBootDashModel<DockerClient, DockerTargetParams> model = createDockerTarget();
IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"));
createExeFile(project, "sts-docker-build.sh",
"#!/bin/bash\n" +
"docker build -t webby ."
);
createFile(project, "Dockerfile",
"FROM openjdk:11 as builder\n" +
"CMD mkdir /source\n" +
"COPY . /source\n" +
"WORKDIR /source\n" +
"RUN ls -la\n" +
"RUN ./mvnw clean package\n" +
"FROM openjdk:11\n" +
"COPY --from=builder /source/target/*.jar /app.jar\n" +
"ENTRYPOINT java $JAVA_OPTS -jar /app.jar"
);
dragAndDrop(project, model);
GenericRemoteAppElement dep = waitForDeployment(model, project);
GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage);
GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer);
assertContains("webby:latest", img.getStyledName(null).getString());
ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> {
assertEquals(RunState.RUNNING, dep.getRunState());
assertEquals(RunState.RUNNING, img.getRunState());
assertEquals(RunState.RUNNING, con.getRunState());
});
}
@Test
public void missingBuildTagException() throws Exception {
GenericRemoteBootDashModel<DockerClient, DockerTargetParams> model = createDockerTarget();
IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"));
createExeFile(project, "sts-docker-build.sh",
"#!/bin/bash\n" +
"# do nothing"
);
dragAndDrop(project, model);
GenericRemoteAppElement dep = waitForDeployment(model, project);
ACondition.waitFor("error marker", 3_000, () -> {
RefreshState s = dep.getRefreshState();
assertTrue(s.isError());
assertEquals("MissingBuildTagException: Couldn't detect the image id or tag", s.getMessage());
});
}
@Test
public void projectWithdDockerFileNoTag() throws Exception {
GenericRemoteBootDashModel<DockerClient, DockerTargetParams> model = createDockerTarget();
IProject project = projects.createBootWebProject("webby", bootVersionAtLeast("2.3.0"));
createExeFile(project, "sts-docker-build.sh",
"#!/bin/bash\n" +
"docker build ."
);
createFile(project, "Dockerfile",
"FROM openjdk:11 as builder\n" +
"CMD mkdir /source\n" +
"COPY . /source\n" +
"WORKDIR /source\n" +
"RUN ls -la\n" +
"RUN ./mvnw clean package\n" +
"FROM openjdk:11\n" +
"COPY --from=builder /source/target/*.jar /app.jar\n" +
"ENTRYPOINT java $JAVA_OPTS -jar /app.jar"
);
dragAndDrop(project, model);
GenericRemoteAppElement dep = waitForDeployment(model, project);
GenericRemoteAppElement img = waitForChild(dep, d -> d instanceof DockerImage);
GenericRemoteAppElement con = waitForChild(img, d -> d instanceof DockerContainer);
ACondition.waitFor("all started", BUILD_IMAGE_TIMEOUT, () -> {
assertEquals(RunState.RUNNING, dep.getRunState());
assertEquals(RunState.RUNNING, img.getRunState());
assertEquals(RunState.RUNNING, con.getRunState());
});
}
@Test
public void devtoolsFullScenario() throws Exception {
GenericRemoteBootDashModel<DockerClient, DockerTargetParams> model = createDockerTarget();