From 5889fa482a4fa1b9ba9b465e85f870c958b2882f Mon Sep 17 00:00:00 2001 From: Jan-Willem Gmelig Meyling Date: Wed, 31 Dec 2014 03:07:13 +0100 Subject: [PATCH 1/2] Spotify Docker client for Build Manager --- .gitignore | 3 + build-client/pom.xml | 6 + .../ewi/build/jaxrs/models/BuildRequest.java | 2 +- build-server/pom.xml | 21 + .../tudelft/ewi/build/BuildServerModule.java | 12 +- .../java/nl/tudelft/ewi/build/Config.java | 2 - .../ewi/build/PropertyBasedConfig.java | 4 - .../nl/tudelft/ewi/build/SimpleConfig.java | 18 - .../ewi/build/builds/BuildManager.java | 424 +++++++++++++--- .../ewi/build/builds/BuildResultLogger.java | 39 ++ .../tudelft/ewi/build/builds/BuildRunner.java | 196 -------- .../nl/tudelft/ewi/build/builds/Logger.java | 13 + .../ewi/build/docker/BuildReference.java | 42 -- .../ewi/build/docker/CommandParser.java | 48 -- .../tudelft/ewi/build/docker/Container.java | 79 --- .../ewi/build/docker/ContainerStart.java | 20 - .../ewi/build/docker/DefaultLogger.java | 63 --- .../tudelft/ewi/build/docker/DockerJob.java | 15 - .../ewi/build/docker/DockerManager.java | 40 -- .../ewi/build/docker/DockerManagerImpl.java | 455 ------------------ .../nl/tudelft/ewi/build/docker/Error.java | 15 - .../ewi/build/docker/Identifiable.java | 22 - .../ewi/build/docker/ImageBuildObserver.java | 11 - .../nl/tudelft/ewi/build/docker/Logger.java | 13 - .../nl/tudelft/ewi/build/docker/LxcConf.java | 20 - .../tudelft/ewi/build/docker/StatusCode.java | 11 - .../nl/tudelft/ewi/build/docker/Stream.java | 8 - .../BuildInstructionInterpreter.java | 6 - .../DefaultBuildInstructionInterpreter.java | 30 -- .../MavenBuildInstructionInterpreter.java | 12 +- .../staging/GitStagingDirectoryPreparer.java | 8 +- .../staging/StagingDirectoryPreparer.java | 3 +- .../ewi/build/jaxrs/BuildsResource.java | 105 +++- .../ewi/build/jaxrs/ImagesResource.java | 84 ---- .../src/main/resources/config.properties | 3 +- .../docker/client/MockedLogStream.java | 36 ++ .../ewi/build/builds/BuildManagerTest.java | 87 +++- .../ewi/build/builds/MockedDockerManager.java | 101 ---- .../ewi/build/docker/CommandParserTest.java | 44 -- .../build/docker/DockerManagerImplTest.java | 52 -- pom.xml | 13 +- 41 files changed, 652 insertions(+), 1534 deletions(-) delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/SimpleConfig.java create mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildResultLogger.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildRunner.java create mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/builds/Logger.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/BuildReference.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/CommandParser.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/Container.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/ContainerStart.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/DefaultLogger.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerJob.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManager.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManagerImpl.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/Error.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/Identifiable.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/ImageBuildObserver.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/Logger.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/LxcConf.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/StatusCode.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/docker/Stream.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/DefaultBuildInstructionInterpreter.java delete mode 100644 build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/ImagesResource.java create mode 100644 build-server/src/test/java/com/spotify/docker/client/MockedLogStream.java delete mode 100644 build-server/src/test/java/nl/tudelft/ewi/build/builds/MockedDockerManager.java delete mode 100644 build-server/src/test/java/nl/tudelft/ewi/build/docker/CommandParserTest.java delete mode 100644 build-server/src/test/java/nl/tudelft/ewi/build/docker/DockerManagerImplTest.java diff --git a/.gitignore b/.gitignore index f6aadcb..20ac5c4 100644 --- a/.gitignore +++ b/.gitignore @@ -62,3 +62,6 @@ build-models/.settings build-server/target/ build-server/.settings +.DS_Store +.idea/ +*.iml diff --git a/build-client/pom.xml b/build-client/pom.xml index c571d58..6421d4a 100644 --- a/build-client/pom.xml +++ b/build-client/pom.xml @@ -14,6 +14,12 @@ org.jboss.resteasy resteasy-client ${resteasy.version} + + + slf4j-simple + org.slf4j + + nl.tudelft.ewi.build diff --git a/build-models/src/main/java/nl/tudelft/ewi/build/jaxrs/models/BuildRequest.java b/build-models/src/main/java/nl/tudelft/ewi/build/jaxrs/models/BuildRequest.java index 514676b..67dba2e 100644 --- a/build-models/src/main/java/nl/tudelft/ewi/build/jaxrs/models/BuildRequest.java +++ b/build-models/src/main/java/nl/tudelft/ewi/build/jaxrs/models/BuildRequest.java @@ -11,6 +11,6 @@ public class BuildRequest { private String callbackUrl; - private int timeout; + private Integer timeout; } diff --git a/build-server/pom.xml b/build-server/pom.xml index 6676f3f..5942c4c 100644 --- a/build-server/pom.xml +++ b/build-server/pom.xml @@ -29,6 +29,12 @@ + + + com.spotify + docker-client + 2.7.19 + org.eclipse.jgit org.eclipse.jgit @@ -44,6 +50,21 @@ jtar 1.1 + + org.jboss.resteasy + resteasy-guice + ${resteasy.version} + + + slf4j-simple + org.slf4j + + + httpclient + org.apache.httpcomponents + + + diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/BuildServerModule.java b/build-server/src/main/java/nl/tudelft/ewi/build/BuildServerModule.java index 4d18dad..c1638ed 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/BuildServerModule.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/BuildServerModule.java @@ -7,10 +7,13 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.AbstractModule; +import com.spotify.docker.client.DefaultDockerClient; +import com.spotify.docker.client.DockerClient; + +import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.docker.DockerManager; -import nl.tudelft.ewi.build.docker.DockerManagerImpl; import nl.tudelft.ewi.build.jaxrs.json.MappingModule; + import org.jboss.resteasy.plugins.guice.ext.JaxrsModule; import org.jboss.resteasy.plugins.guice.ext.RequestScopeModule; import org.reflections.Reflections; @@ -25,6 +28,7 @@ public BuildServerModule(Config config) { } @Override + @SneakyThrows protected void configure() { install(new RequestScopeModule()); install(new JaxrsModule()); @@ -39,7 +43,9 @@ public ObjectMapper get() { } }); - bind(DockerManager.class).to(DockerManagerImpl.class); + bind(DockerClient.class).toInstance(DefaultDockerClient.fromEnv() + .readTimeoutMillis(DefaultDockerClient.NO_TIMEOUT) + .build()); findResourcesWith(Path.class); findResourcesWith(Provider.class); diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/Config.java b/build-server/src/main/java/nl/tudelft/ewi/build/Config.java index 422e75b..1ccbfc5 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/Config.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/Config.java @@ -7,8 +7,6 @@ public interface Config { int getMaximumConcurrentJobs(); - String getDockerHost(); - String getStagingDirectory(); String getWorkingDirectory(); diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/PropertyBasedConfig.java b/build-server/src/main/java/nl/tudelft/ewi/build/PropertyBasedConfig.java index bd1ae13..9442495 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/PropertyBasedConfig.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/PropertyBasedConfig.java @@ -33,10 +33,6 @@ public int getMaximumConcurrentJobs() { return Integer.parseInt(properties.getProperty("docker.max-containers")); } - public String getDockerHost() { - return properties.getProperty("docker.host", "http://localhost:4243"); - } - public String getStagingDirectory() { return properties.getProperty("docker.staging-directory"); } diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/SimpleConfig.java b/build-server/src/main/java/nl/tudelft/ewi/build/SimpleConfig.java deleted file mode 100644 index a430eff..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/SimpleConfig.java +++ /dev/null @@ -1,18 +0,0 @@ -package nl.tudelft.ewi.build; - -import lombok.Data; -import lombok.experimental.Accessors; - -@Data -@Accessors(chain = true) -public class SimpleConfig implements Config { - - private int httpPort = 8081; - private int maximumConcurrentJobs = 1; - private String dockerHost = "http://localhost:4243/"; - private String stagingDirectory = "workshop"; - private String workingDirectory = "/workshop"; - private String clientId = "test-client"; - private String clientSecret = "test-secret"; - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildManager.java b/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildManager.java index 92609c3..22c8efd 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildManager.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildManager.java @@ -1,120 +1,390 @@ package nl.tudelft.ewi.build.builds; -import javax.inject.Inject; -import javax.inject.Singleton; - +import java.io.File; +import java.io.IOException; import java.util.Map; import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.FutureTask; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicReference; + +import org.apache.commons.io.FileUtils; +import org.eclipse.jetty.util.component.AbstractLifeCycle.AbstractLifeCycleListener; +import org.eclipse.jetty.util.component.LifeCycle; + +import nl.tudelft.ewi.build.Config; +import nl.tudelft.ewi.build.extensions.instructions.BuildInstructionInterpreter; +import nl.tudelft.ewi.build.extensions.instructions.BuildInstructionInterpreterRegistry; +import nl.tudelft.ewi.build.extensions.staging.StagingDirectoryPreparer; +import nl.tudelft.ewi.build.extensions.staging.StagingDirectoryPreparerRegistry; +import nl.tudelft.ewi.build.jaxrs.models.BuildInstruction; +import nl.tudelft.ewi.build.jaxrs.models.BuildRequest; +import nl.tudelft.ewi.build.jaxrs.models.BuildResult; +import nl.tudelft.ewi.build.jaxrs.models.Source; +import nl.tudelft.ewi.build.jaxrs.models.BuildResult.Status; +import lombok.extern.slf4j.Slf4j; import com.google.common.collect.Maps; +import com.google.common.util.concurrent.AbstractFuture; import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; +import com.google.common.util.concurrent.ListeningScheduledExecutorService; import com.google.common.util.concurrent.MoreExecutors; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.Config; -import nl.tudelft.ewi.build.docker.DockerManager; -import nl.tudelft.ewi.build.jaxrs.models.BuildRequest; +import com.google.inject.Inject; +import com.google.inject.Singleton; +import com.spotify.docker.client.DockerClient; +import com.spotify.docker.client.DockerException; +import com.spotify.docker.client.LogStream; +import com.spotify.docker.client.DockerClient.AttachParameter; +import com.spotify.docker.client.messages.ContainerConfig; +import com.spotify.docker.client.messages.ContainerCreation; +import com.spotify.docker.client.messages.ContainerExit; +import com.spotify.docker.client.messages.HostConfig; @Slf4j @Singleton -public class BuildManager { +public class BuildManager extends AbstractLifeCycleListener implements LifeCycle.Listener { - private final Map> futures; - private final Map runners; + private final static String WORK_DIR = "/workdir"; - private final ListeningExecutorService listeningService; - private final ScheduledExecutorService schedulerService; - private final DockerManager dockerManager; private final Config config; + private final DockerClient dockerClient; + private final StagingDirectoryPreparerRegistry stagingDirectoryPreparerRegistry; + private final BuildInstructionInterpreterRegistry buildInstructionInterpreterRegistry; + private final Map> builds; + private final ListeningScheduledExecutorService executor; @Inject - public BuildManager(DockerManager dockerManager, Config config) { - this.futures = Maps.newConcurrentMap(); - this.runners = Maps.newConcurrentMap(); - - this.schedulerService = Executors.newScheduledThreadPool(config.getMaximumConcurrentJobs()); - this.listeningService = MoreExecutors.listeningDecorator(schedulerService); - this.dockerManager = dockerManager; + public BuildManager( + final Config config, + final DockerClient dockerClient, + final StagingDirectoryPreparerRegistry stagingDirectoryPreparerRegistry, + final BuildInstructionInterpreterRegistry buildInstructionInterpreterRegistry) { + + super(); this.config = config; + this.dockerClient = dockerClient; + this.stagingDirectoryPreparerRegistry = stagingDirectoryPreparerRegistry; + this.buildInstructionInterpreterRegistry = buildInstructionInterpreterRegistry; + this.builds = Maps.newHashMap(); + this.executor = MoreExecutors.listeningDecorator(Executors + .newScheduledThreadPool(2 * config.getMaximumConcurrentJobs())); } - - public UUID schedule(BuildRequest request) { - log.info("Submitted job: " + request); - if (futures.size() >= config.getMaximumConcurrentJobs()) { - log.info("Server is too busy!"); + + /** + * Schedule a new build + * @param buildRequest {@link BuildRequest} object that describes the build + * @return {@link UUID} which identifies the build + * @see #killBuild(UUID) + */ + public Build schedule(final BuildRequest buildRequest) { + if(builds.size() >= config.getMaximumConcurrentJobs()) { return null; } + + return new Build(buildRequest); + } + + /** + * Kill a scheduled build + * @param uuid {@link UUID} that identifies the build + * @see #schedule(BuildRequest) + */ + public void killBuild(final UUID uuid) { + final Future build = builds.remove(uuid); + if (build == null) + throw new IllegalArgumentException("Build does not exist!"); + else + build.cancel(true); + } + + /** + * Get the {@link Future} for a certain build + * @param uuid {@link UUID} that identifies the build + * @return the {@link Future} for the build + * @see #schedule(BuildRequest) + */ + public ListenableFuture getBuild(final UUID uuid) { + return builds.get(uuid); + } + + @Override + public void lifeCycleStopping(LifeCycle event) { + executor.shutdown(); + } + + /** + * The {@link Build} class forms the {@link Future} for the + * {@link BuildRequest}, which completes even if the build fails, or the + * container exits abnormally due to an error, timeout or cancellation. In + * this case the build {@link Status} is set to {@code FAILED}. + * + * @author Jan-Willem Gmelig Meyling + * + */ + public class Build extends AbstractFuture + implements ListenableFuture, Runnable { - UUID buildId = UUID.randomUUID(); - BuildRunner runner = new BuildRunner(dockerManager, config, request, buildId); - ListenableFuture future = listeningService.submit(runner); - futures.put(buildId, future); - runners.put(buildId, runner); + private final BuildRequest buildRequest; + private final BuildResult buildResult; + private final Logger logger; + private final BuildTask buildTask; - int timeout = request.getTimeout(); - if (timeout > 0) { - log.debug("Build will automatically terminate in: {} seconds...", timeout); - Runnable terminator = createTerminator(buildId); - final Future terminatorFuture = schedulerService.schedule(terminator, timeout, TimeUnit.SECONDS); - future.addListener(cancelTerminator(buildId, terminatorFuture), schedulerService); + Build(final BuildRequest buildRequest) { + this.buildRequest = buildRequest; + this.buildResult = new BuildResult(); + this.logger = new BuildResultLogger(buildResult); + this.buildTask = new BuildTask(new BuildRunner(logger, buildRequest)); + start(); + } + + private final void start() { + executor.execute(buildTask); + executor.execute(this); + builds.put(getUUID(), this); + } + + @Override + public void run() { + try { + Integer timeout = buildRequest.getTimeout(); + ContainerExit exit = timeout == null || timeout == 0 ? buildTask.get() + : buildTask.get(timeout, TimeUnit.SECONDS); + Status status = exit.statusCode() == 0 ? Status.SUCCEEDED + : Status.FAILED; + buildResult.setStatus(status); + } + catch (TimeoutException e) { + buildTask.cancel(true); + logger.println("[FATAL] Build timed out!"); + buildResult.setStatus(Status.FAILED); + log.info("Build timed out {}", getUUID()); + } + catch (CancellationException e) { + logger.println("[FATAL] Build was cancelled!"); + buildResult.setStatus(Status.FAILED); + log.info("Build cancelled " + getUUID()); + } + catch (Throwable t) { + buildResult.setStatus(Status.FAILED); + logger.println("[FATAL] An exception occured during the build!"); + log.warn("Build task failed " + getUUID(), t); + } + finally { + logger.close(); + builds.remove(getUUID()); + } + set(buildResult); + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + if(super.cancel(mayInterruptIfRunning)) { + // Cancel the build as well + buildTask.cancel(mayInterruptIfRunning); + return true; + } + return false; + } + + public UUID getUUID() { + return buildTask.getUUID(); } - - future.addListener(cleaner(buildId), schedulerService); - return buildId; } - private Runnable cleaner(final UUID buildId) { - return new Runnable() { - @Override - public void run() { - futures.remove(buildId); - runners.remove(buildId); + /** + * The {@link BuildTask} wraps a {@link BuildRunner} in a {@link FutureTask} + * , to enforce killing the container on cancellation and removing the + * container after the {@link Future} is done. + * + * @author Jan-Willem Gmelig Meyling + * + */ + class BuildTask extends FutureTask { + + private final BuildRunner buildRunner; + + BuildTask(final BuildRunner buildRunner) { + super(buildRunner); + this.buildRunner = buildRunner; + } + + @Override + protected void done() { + if(isCancelled()) { + buildRunner.kill(); } - }; + buildRunner.remove(); + builds.remove(getUUID()); + } + + public UUID getUUID() { + return buildRunner.getUUID(); + } + } - private Runnable cancelTerminator(final UUID buildId, final Future terminatorFuture) { - return new Runnable() { - @Override - public void run() { - Future future = futures.remove(buildId); - if (future != null && !future.isDone() && !future.isCancelled()) { - log.debug("Cancelling terminator for build: {}", buildId); - terminatorFuture.cancel(true); + /** + * The {@link BuildRunner} is a {@link Callable} responsible for creating + * and watching the Docker container. + * + * @author Jan-Willem Gmelig Meyling + * + */ + class BuildRunner implements Callable { + + private final UUID uuid; + private final Logger logger; + private final BuildRequest buildRequest; + private final AtomicReference stagingDirectoryReference; + private final AtomicReference containerId; + private final AtomicReference started; + + BuildRunner(final Logger logger, final BuildRequest buildRequest) { + this.stagingDirectoryReference = new AtomicReference(); + this.containerId = new AtomicReference(); + this.started = new AtomicReference(false); + this.buildRequest = buildRequest; + this.uuid = UUID.randomUUID(); + this.logger = logger; + } + + @Override + public ContainerExit call() throws Exception { + if(!started.compareAndSet(false, true)) + throw new IllegalStateException("DockerRunner is already running!"); + + File stagingDirectory = createStagingDirectory(); + stagingDirectoryReference.set(stagingDirectory); + prepareStagingDirectory(stagingDirectory); + + String volume = String.format("%s:%s", stagingDirectory, WORK_DIR); + + BuildInstructionInterpreter buildInstructionInterpreter = + getBuildIntstructionInterpreter(); + BuildInstruction buildInstruction = buildRequest.getInstruction(); + + ContainerConfig.Builder configBuilder = ContainerConfig.builder() + .image(buildInstructionInterpreter.getImage(buildInstruction)) + .cmd(buildInstructionInterpreter.getCommand(buildInstruction).split(" ")) + .volumes(volume) + .workingDir(WORK_DIR); + + String id; + + try { + log.info("Create container {}", config); + ContainerCreation creation = dockerClient.createContainer(configBuilder.build(), uuid.toString()); + id = creation.id(); + containerId.set(id); + log.info("Starting container {}", id); + dockerClient.startContainer(id, HostConfig.builder().binds(volume).build()); + } + catch (DockerException | InterruptedException e) { + logger.println("[FATAL] Failed to provision build environment"); + throw e; + } + + try(LogStream stream = dockerClient.attachContainer(id, AttachParameter.LOGS, + AttachParameter.STDERR, AttachParameter.STDOUT, AttachParameter.STREAM)) { + log.info("Attaching log for container {}", id); + while(stream.hasNext() && !Thread.currentThread().isInterrupted()) + logger.consume(stream.next()); + } + + log.info("Waiting for container to terminate {}", id); + return dockerClient.waitContainer(id); + } + + /** + * Kill the Docker container. This is called when the Future was cancelled + * and thus the container did not exit yet. + */ + public void kill() { + String id = containerId.get(); + if(id != null) { + log.info("Trying to kill container {}", id); + try { + dockerClient.killContainer(id); + } + catch (DockerException | InterruptedException e) { + log.warn("Failed to kill container " + id, e); } } - }; - } + } - private Runnable createTerminator(final UUID buildId) { - return new Runnable() { - @Override - public void run() { - log.debug("Forcefully terminating build: {}", buildId); - killBuild(buildId); + /** + * Remove the Docker container. This is called when the Future is either + * cancelled or completed. This also removes the staging directory that + * was shared with the container. + */ + public void remove() { + String id = containerId.get(); + if(id != null) { + log.info("Trying to remove container {}", id); + try { + dockerClient.removeContainer(id, true); + } + catch (DockerException | InterruptedException e) { + log.info("Failed to remove container " + id, e); + } } - }; - } + + removeStagingDirectory(); + } - @SneakyThrows - public boolean killBuild(UUID buildId) { - BuildRunner runner = runners.remove(buildId); - ListenableFuture future = futures.remove(buildId); + protected File createStagingDirectory() throws IOException { + try { + File stagingDirectory = new File(config.getStagingDirectory(), uuid.toString()); + log.info("Created staging directory: {}", stagingDirectory.getAbsolutePath()); + stagingDirectory.mkdirs(); + return stagingDirectory; + } + catch (Throwable e) { + logger.println("[FATAL] Failed to allocate new working directory for build"); + throw new IOException(e); + } + } - if (future != null) { - future.cancel(true); - if (runner != null) { - runner.terminate(); + protected void removeStagingDirectory() { + File dir = stagingDirectoryReference.get(); + if(dir != null && dir.exists()) { + try { + log.info("Removing staging directory {}", stagingDirectoryReference); + FileUtils.deleteDirectory(dir); + } + catch (IOException e) { + log.warn("Failed to cleanup staging directory " + stagingDirectoryReference, e); + } } - return true; } - return false; + + @SuppressWarnings("unchecked") + protected void prepareStagingDirectory(final File stagingDirectory) throws IOException { + Source source = buildRequest.getSource(); + StagingDirectoryPreparer preparer = + (StagingDirectoryPreparer) + stagingDirectoryPreparerRegistry.getStagingDirectoryPreparer(source.getClass()); + log.info("Preparing staging directory {} with source: {}", stagingDirectory, source); + preparer.prepareStagingDirectory(source, logger, stagingDirectory); + } + + @SuppressWarnings("unchecked") + protected BuildInstructionInterpreter getBuildIntstructionInterpreter() { + BuildInstruction instruction = buildRequest.getInstruction(); + return (BuildInstructionInterpreter) buildInstructionInterpreterRegistry + .getBuildDecorator(instruction.getClass()); + } + + public UUID getUUID() { + return uuid; + } + } } diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildResultLogger.java b/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildResultLogger.java new file mode 100644 index 0000000..9391290 --- /dev/null +++ b/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildResultLogger.java @@ -0,0 +1,39 @@ +package nl.tudelft.ewi.build.builds; + +import static com.google.common.base.Charsets.UTF_8; + +import java.util.List; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import com.spotify.docker.client.LogMessage; + +import nl.tudelft.ewi.build.jaxrs.models.BuildResult; + +public class BuildResultLogger implements Logger { + + private final List lines; + + public BuildResultLogger(final BuildResult buildResult) { + Preconditions.checkNotNull(buildResult); + this.lines = Lists.newArrayList(); + buildResult.setLogLines(lines); + } + + public void println(String content) { + synchronized(lines) { + this.lines.add(content); + } + } + + @Override + public void close() { + // no-op + } + + @Override + public void consume(LogMessage message) { + println(UTF_8.decode(message.content()).toString()); + } + +} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildRunner.java b/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildRunner.java deleted file mode 100644 index 1004b95..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/builds/BuildRunner.java +++ /dev/null @@ -1,196 +0,0 @@ -package nl.tudelft.ewi.build.builds; - -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.client.Invocation.Builder; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.Response.StatusType; - -import java.io.File; -import java.io.IOException; -import java.util.UUID; - -import org.eclipse.jgit.util.FileUtils; - -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import lombok.Data; -import lombok.SneakyThrows; -import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.Config; -import nl.tudelft.ewi.build.docker.BuildReference; -import nl.tudelft.ewi.build.docker.DefaultLogger; -import nl.tudelft.ewi.build.docker.DefaultLogger.OnClose; -import nl.tudelft.ewi.build.docker.DockerJob; -import nl.tudelft.ewi.build.docker.DockerManager; -import nl.tudelft.ewi.build.docker.Identifiable; -import nl.tudelft.ewi.build.docker.Logger; -import nl.tudelft.ewi.build.extensions.instructions.BuildInstructionInterpreter; -import nl.tudelft.ewi.build.extensions.instructions.BuildInstructionInterpreterRegistry; -import nl.tudelft.ewi.build.extensions.staging.StagingDirectoryPreparer; -import nl.tudelft.ewi.build.extensions.staging.StagingDirectoryPreparerRegistry; -import nl.tudelft.ewi.build.jaxrs.models.BuildInstruction; -import nl.tudelft.ewi.build.jaxrs.models.BuildRequest; -import nl.tudelft.ewi.build.jaxrs.models.BuildResult; -import nl.tudelft.ewi.build.jaxrs.models.Source; -import org.jboss.resteasy.util.Base64; - -@Data -@Slf4j -class BuildRunner implements Runnable { - - private final DockerManager docker; - private final Config config; - private final BuildRequest request; - private final UUID identifier; - - private final DefaultLogger logger = new DefaultLogger(); - - @Override - public void run() { - File stagingDirectory = null; - try { - stagingDirectory = createStagingDirectory(logger); - - final File stagingDirRef = stagingDirectory; - logger.onClose(new OnClose() { - @Override - public void onClose() { - BuildResult result = createBuildResult(logger, stagingDirRef); - broadcastResultThroughCallback(result); - } - }); - - prepareStagingDirectory(logger, stagingDirectory); - startDockerJob(logger, stagingDirectory.getAbsolutePath()); - } - catch (Throwable e) { - log.error(e.getMessage(), e); - logger.onClose(-1); - } - finally { - try { - FileUtils.delete(stagingDirectory, FileUtils.RECURSIVE); - } - catch (IOException e) { - log.error(e.getMessage(), e); - } - } - } - - public void terminate() { - Identifiable identifiable = logger.getContainer(); - if (identifiable != null) { - log.warn("Issueing container termination to docker: {}", identifiable); - docker.terminate(identifiable); - } - } - - private File createStagingDirectory(Logger logger) throws IOException { - File stagingDirectory = new File(config.getStagingDirectory(), identifier.toString()); - try { - log.info("Created staging directory: {}", stagingDirectory.getAbsolutePath()); - stagingDirectory.mkdirs(); - return stagingDirectory; - } - catch (Throwable e) { - logger.onNextLine("[FATAL] Failed to allocate new working directory for build"); - throw new IOException(e); - } - } - - private void startDockerJob(Logger logger, String stagingDirectory) throws Throwable { - BuildInstruction instruction = request.getInstruction(); - BuildInstructionInterpreter buildDecorator = createBuildDecorator(); - - DockerJob job = new DockerJob(); - job.setCommand(buildDecorator.getCommand(instruction)); - job.setImage(buildDecorator.getImage(instruction)); - job.setWorkingDirectory(config.getWorkingDirectory()); - job.setMounts(ImmutableMap.of(stagingDirectory, config.getWorkingDirectory())); - - try { - log.info("Starting docker job: {}", instruction); - BuildReference build = docker.run(logger, job); - logger.initialize(build.getContainerId()); - build.awaitTermination(); - } - catch (Throwable e) { - logger.onNextLine("[FATAL] Failed to provision new build environment"); - throw e; - } - } - - private void prepareStagingDirectory(DefaultLogger logger, File stagingDirectory) throws IOException { - Source source = request.getSource(); - log.info("Preparing staging directory with source: {}", source); - createStagingDirectoryPreparer().prepareStagingDirectory(source, logger, stagingDirectory); - } - - private BuildResult createBuildResult(DefaultLogger logger, File stagingDirectory) { - BuildInstruction instruction = request.getInstruction(); - log.info("Creating build result according to instruction: {}", instruction); - return createBuildDecorator().createResult(instruction, logger, stagingDirectory); - } - - @SuppressWarnings("unchecked") - private BuildInstructionInterpreter createBuildDecorator() { - BuildInstruction instruction = request.getInstruction(); - BuildInstructionInterpreterRegistry registry = new BuildInstructionInterpreterRegistry(); - return (BuildInstructionInterpreter) registry.getBuildDecorator(instruction.getClass()); - } - - @SuppressWarnings("unchecked") - private StagingDirectoryPreparer createStagingDirectoryPreparer() { - Source source = request.getSource(); - StagingDirectoryPreparerRegistry registry = new StagingDirectoryPreparerRegistry(); - return (StagingDirectoryPreparer) registry.getStagingDirectoryPreparer(source.getClass()); - } - - @SneakyThrows - private void broadcastResultThroughCallback(BuildResult result) { - if (Strings.isNullOrEmpty(request.getCallbackUrl())) { - return; - } - - log.info("Returning build results to callback URL: {}", request.getCallbackUrl()); - for (int i = 0; i <= 4; i++) { - Client client = ClientBuilder.newClient(); - try { - Response response = prepareCallback(client).post(Entity.json(result)); - StatusType statusInfo = response.getStatusInfo(); - if (statusInfo.getStatusCode() >= 200 && statusInfo.getStatusCode() < 300) { - log.info("Build result successfully returned to: {}", request.getCallbackUrl()); - return; - } - log.warn("Could not return build result to: {}, status was: {} - {}", request.getCallbackUrl(), - response.getStatus(), statusInfo.getReasonPhrase()); - } - catch (Throwable e) { - log.warn(e.getMessage(), e); - } - finally { - if (client != null) { - client.close(); - } - } - - // Exponential backoff. - if (i < 4) { - Thread.sleep(5000L * 2 ^ i); - } - } - - log.error("Could not return build result to: {}", request.getCallbackUrl()); - } - - private Builder prepareCallback(Client client) { - String userPass = config.getClientId() + ":" + config.getClientSecret(); - String authorization = "Basic " + Base64.encodeBytes(userPass.getBytes()); - return client.target(request.getCallbackUrl()) - .request() - .header("Authorization", authorization); - } - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/builds/Logger.java b/build-server/src/main/java/nl/tudelft/ewi/build/builds/Logger.java new file mode 100644 index 0000000..3dbafeb --- /dev/null +++ b/build-server/src/main/java/nl/tudelft/ewi/build/builds/Logger.java @@ -0,0 +1,13 @@ +package nl.tudelft.ewi.build.builds; + +import com.spotify.docker.client.LogMessage; + +public interface Logger extends AutoCloseable { + + void consume(LogMessage message); + + void println(String string); + + @Override void close(); + +} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/BuildReference.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/BuildReference.java deleted file mode 100644 index c491ceb..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/BuildReference.java +++ /dev/null @@ -1,42 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; - -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class BuildReference { - - private final DockerJob job; - private final Identifiable containerId; - private final Future future; - - public BuildReference(DockerJob job, Identifiable containerId, Future future) { - this.job = job; - this.containerId = containerId; - this.future = future; - } - - public DockerJob getJob() { - return job; - } - - public Identifiable getContainerId() { - return containerId; - } - - public void awaitTermination() { - try { - future.get(); - } - catch (InterruptedException | ExecutionException e) { - log.error(e.getMessage(), e); - } - } - - public boolean terminate() { - return future.cancel(true); - } - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/CommandParser.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/CommandParser.java deleted file mode 100644 index cf06e86..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/CommandParser.java +++ /dev/null @@ -1,48 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.List; - -import lombok.AccessLevel; -import lombok.NoArgsConstructor; - -import com.google.common.collect.Lists; - -@NoArgsConstructor(access = AccessLevel.NONE) -class CommandParser { - - public static List parse(String command) { - List commands = Lists.newArrayList(); - - int characterIndex = 0; - boolean inDoubleQuotes = false; - - while (characterIndex < command.length()) { - char character = command.charAt(characterIndex); - if (character == '\"') { - int lastCommand = commands.size() - 1; - String last = lastCommand >= 0 ? commands.remove(lastCommand) : ""; - commands.add(last + character); - inDoubleQuotes = !inDoubleQuotes; - } - else if (character == ' ') { - if (inDoubleQuotes) { - int lastCommand = commands.size() - 1; - String last = lastCommand >= 0 ? commands.remove(lastCommand) : ""; - commands.add(last + character); - } - else { - commands.add(""); - } - } - else { - int lastCommand = commands.size() - 1; - String last = lastCommand >= 0 ? commands.remove(lastCommand) : ""; - commands.add(last + character); - } - characterIndex++; - } - - return commands; - } - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Container.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/Container.java deleted file mode 100644 index 38ccbe0..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Container.java +++ /dev/null @@ -1,79 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.List; - -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; - -import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.Data; -import lombok.experimental.Accessors; - -@Data -@Accessors(chain = true) -@JsonIgnoreProperties(ignoreUnknown = true) -public class Container { - - @JsonProperty("Id") - private String id = ""; - - @JsonProperty("Hostname") - private String hostname = ""; - - @JsonProperty("User") - private String user = ""; - - @JsonProperty("Memory") - private Integer memory = 0; - - @JsonProperty("MemorySwap") - private Integer memorySwap = 0; - - @JsonProperty("AttachStdin") - private Boolean attachStdin = false; - - @JsonProperty("AttachStdout") - private Boolean attachStdout = false; - - @JsonProperty("AttachStderr") - private Boolean attachStderr = false; - - @JsonProperty("PortSpecs") - private Object portSpecs = null; - - @JsonProperty("Tty") - private Boolean tty = false; - - @JsonProperty("OpenStdin") - private Boolean openStdin = false; - - @JsonProperty("StdinOnce") - private Boolean stdinOnce = false; - - @JsonProperty("Env") - private Object env = null; - - @JsonProperty("Cmd") - private List cmd; - - @JsonProperty("Dns") - private Object dns = null; - - @JsonProperty("Image") - private String image; - - @JsonProperty("Volumes") - private Object volumes = null; - - @JsonProperty("VolumesFrom") - private String volumesFrom = ""; - - @JsonProperty("WorkingDir") - private String workingDir = ""; - - @JsonProperty("ExposedPorts") - private Object exposedPorts = null; - - @JsonProperty("Status") - private String status = ""; - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/ContainerStart.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/ContainerStart.java deleted file mode 100644 index 589f403..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/ContainerStart.java +++ /dev/null @@ -1,20 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.List; - -import lombok.Data; -import lombok.experimental.Accessors; - -import com.fasterxml.jackson.annotation.JsonProperty; - -@Data -@Accessors(chain = true) -public class ContainerStart { - - @JsonProperty("Binds") - private List binds; - - @JsonProperty("LxcConf") - private List lxcConf; - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DefaultLogger.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/DefaultLogger.java deleted file mode 100644 index fbb5ea3..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DefaultLogger.java +++ /dev/null @@ -1,63 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicReference; - -import lombok.Data; -import lombok.extern.slf4j.Slf4j; -import com.google.common.collect.Lists; - -@Data -@Slf4j -public class DefaultLogger implements Logger { - - public static interface OnClose { - void onClose(); - } - - private final List logLines = Lists.newArrayList(); - private final AtomicInteger exitCode = new AtomicInteger(); - private final AtomicBoolean terminated = new AtomicBoolean(); - private final List onCloseCallbacks = Lists.newArrayList(); - private final AtomicReference container = new AtomicReference(); - - @Override - public void initialize(Identifiable container) { - this.container.compareAndSet(null, container); - } - - public Identifiable getContainer() { - return container.get(); - } - - @Override - public void onStart() { - // Do nothing. - } - - @Override - public void onNextLine(String line) { - log.trace("{} >>> {}", container.get(), line); - logLines.add(line); - } - - @Override - public void onClose(int statusCode) { - if (terminated.compareAndSet(false, true)) { - exitCode.set(statusCode); - for (OnClose callback : onCloseCallbacks) { - callback.onClose(); - } - } - else { - log.warn("DefaultLogger was already closed..."); - } - } - - public void onClose(OnClose onCloseCallback) { - onCloseCallbacks.add(onCloseCallback); - } - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerJob.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerJob.java deleted file mode 100644 index bced746..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerJob.java +++ /dev/null @@ -1,15 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.Map; - -import lombok.Data; - -@Data -public class DockerJob { - - private String command; - private String image; - private String workingDirectory; - private Map mounts; - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManager.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManager.java deleted file mode 100644 index 1d7883a..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManager.java +++ /dev/null @@ -1,40 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.io.IOException; - - -public interface DockerManager { - - void buildImage(String name, String dockerFileContents, ImageBuildObserver observer) throws IOException; - - /** - * Starts a new container in Docker and attaches the specified - * {@link Logger}. - * - * @param logger - * The {@link Logger} to attach. This object will store logs - * caught while listening and the exit code upon the container's - * termination. - * @param job - * The {@link DockerJob} describing the container setup and the - * sort of job to run inside the container. - * @return A {@link BuildReference} which allows the requester to terminate - * the container or retrieve information about the container. - */ - BuildReference run(Logger logger, DockerJob job); - - /** - * Terminates a running Docker container. - * - * @param container - * The id of the container to terminate. - */ - void terminate(Identifiable container); - - /** - * @return The number of currently running containers according to the - * Docker service. - */ - int getActiveJobs(); - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManagerImpl.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManagerImpl.java deleted file mode 100644 index c14c764..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/DockerManagerImpl.java +++ /dev/null @@ -1,455 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import javax.inject.Inject; -import javax.ws.rs.ClientErrorException; -import javax.ws.rs.InternalServerErrorException; -import javax.ws.rs.NotFoundException; -import javax.ws.rs.client.Client; -import javax.ws.rs.client.ClientBuilder; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.MediaType; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.BufferedReader; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.FileWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.net.URLEncoder; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.google.common.base.Preconditions; -import com.google.common.base.Strings; -import com.google.common.collect.ImmutableMap; -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.io.Files; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.Config; -import org.xeustechnologies.jtar.TarEntry; -import org.xeustechnologies.jtar.TarOutputStream; - -@Slf4j -public class DockerManagerImpl implements DockerManager { - - private final ListeningExecutorService executor; - private final Config config; - - @Inject - public DockerManagerImpl(Config config) { - int poolSize = config.getMaximumConcurrentJobs() * 2; - this.executor = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(poolSize)); - this.config = config; - } - - @Override - public BuildReference run(final Logger logger, final DockerJob job) { - final String host = config.getDockerHost(); - final Identifiable containerId = create(host, job); - - final Future future = executor.submit(new Runnable() { - @Override - public void run() { - start(host, containerId, job); - logger.onStart(); - - Future logFuture = fetchLog(host, containerId, logger); - StatusCode code = awaitTermination(host, containerId); - logFuture.cancel(true); - - stopAndDelete(host, containerId); - logger.onClose(code.getStatusCode()); - } - }); - - return new BuildReference(job, containerId, new Future() { - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - if (future.cancel(mayInterruptIfRunning)) { - log.warn("Terminating container: {} because it was cancelled.", containerId); - stopAndDelete(host, containerId); - log.warn("Container: {} was terminated forcefully.", containerId); - return true; - } - return false; - } - - @Override - public boolean isCancelled() { - return future.isCancelled(); - } - - @Override - public boolean isDone() { - return future.isDone(); - } - - @Override - public Object get() throws InterruptedException, ExecutionException { - return future.get(); - } - - @Override - public Object get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return future.get(timeout, unit); - } - }); - } - - @Override - public void terminate(Identifiable container) { - String host = config.getDockerHost(); - stopAndDelete(host, container); - } - - @Override - public int getActiveJobs() { - int counter = 0; - for (Container container : getContainers().values()) { - if (!Strings.emptyToNull(container.getStatus()).startsWith("Exit ")) { - return counter++; - } - } - return counter; - } - - @Override - public void buildImage(String name, String dockerFileContents, ImageBuildObserver observer) throws IOException { - Preconditions.checkArgument(!Strings.isNullOrEmpty(name)); - Preconditions.checkArgument(!Strings.isNullOrEmpty(dockerFileContents)); - - name = URLEncoder.encode(name, "UTF-8"); - - File tempDir = Files.createTempDir(); - File archive = new File(tempDir, "image.tar"); - File dockerFile = new File(tempDir, "Dockerfile"); - - FileWriter writer = new FileWriter(dockerFile); - writer.write(dockerFileContents); - writer.flush(); - writer.close(); - - try (TarOutputStream out = new TarOutputStream(new BufferedOutputStream(new FileOutputStream(archive)))) { - out.putNextEntry(new TarEntry(dockerFile, dockerFile.getName())); - - int count; - byte data[] = new byte[2048]; - try (BufferedInputStream origin = new BufferedInputStream(new FileInputStream(dockerFile))) { - while((count = origin.read(data)) != -1) { - out.write(data, 0, count); - } - } - out.flush(); - } - - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Requesting image to be built..."); - InputStream output = client.target(config.getDockerHost() + "/build?t=" + name + "&nocache=true&forcerm=true") - .request("application/tar") - .post(Entity.entity(archive, "application/tar"), InputStream.class); - - ObjectMapper mapper = new ObjectMapper(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(output))) { - String line; - while ((line = reader.readLine()) != null) { - if (line.startsWith("{\"stream\":")) { - observer.onMessage(mapper.readValue(line, Stream.class).getStream()); - } - else if (line.startsWith("{\"error\":")) { - observer.onError(mapper.readValue(line, Error.class).getErrorDetail().getMessage()); - } - } - } - } - finally { - observer.onCompleted(); - if (client != null) { - client.close(); - } - - archive.delete(); - dockerFile.delete(); - tempDir.delete(); - } - } - - private Map getContainers() { - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Listing containers..."); - List containers = client.target(config.getDockerHost() + "/containers/json") - .request(MediaType.APPLICATION_JSON) - .get(new GenericType>() { }); - - Map mapping = Maps.newLinkedHashMap(); - for (Container container : containers) { - Identifiable identifiable = new Identifiable(); - identifiable.setId(container.getId()); - mapping.put(identifiable, container); - } - return mapping; - } - finally { - if (client != null) { - client.close(); - } - } - } - - private Identifiable create(final String host, DockerJob job) { - Map volumes = Maps.newHashMap(); - if (job.getMounts() != null) { - for (String mount : job.getMounts() - .values()) { - volumes.put(mount, ImmutableMap. of()); - } - } - - Container container = new Container().setTty(true) - .setCmd(CommandParser.parse(job.getCommand())) - .setWorkingDir(job.getWorkingDirectory()) - .setVolumes(volumes) - .setImage(job.getImage()); - - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Creating container: {}", container); - return client.target(host + "/containers/create") - .request(MediaType.APPLICATION_JSON) - .post(Entity.json(container), Identifiable.class); - } - finally { - if (client != null) { - client.close(); - } - } - } - - private void start(final String host, final Identifiable container, final DockerJob job) { - Client client = null; - try { - client = ClientBuilder.newClient(); - List mounts = Lists.newArrayList(); - if (job.getMounts() != null) { - for (Entry mount : job.getMounts() - .entrySet()) { - mounts.add(mount.getKey() + ":" + mount.getValue() + ":rw"); - } - } - - ContainerStart start = new ContainerStart().setBinds(mounts) - .setLxcConf(Lists.newArrayList(new LxcConf("lxc.utsname", "docker"))); - - log.debug("Starting container: {} -> {}", container.getId(), start); - client.target(host + "/containers/" + container.getId() + "/start") - .request(MediaType.APPLICATION_JSON) - .accept(MediaType.TEXT_PLAIN) - .post(Entity.json(start)); - } - finally { - if (client != null) { - client.close(); - } - } - } - - private Future fetchLog(final String host, final Identifiable container, final Logger collector) { - return executor.submit(new Runnable() { - @Override - public void run() { - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Streaming log from container: {}", container.getId()); - - String url = "/containers/" + container.getId() + "/attach?logs=1&stream=1&stdout=1&stderr=1"; - final InputStream logs = client.target(host + url) - .request() - .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE) - .post(null, InputStream.class); - - try (InputStreamReader reader = new InputStreamReader(logs)) { - boolean finished = false; - StringBuilder builder = new StringBuilder(); - while (!finished) { - int i = reader.read(); - if (i == -1) { - collector.onNextLine(builder.toString()); - break; - } - - char c = (char) i; - if (c == '\n' || finished) { - collector.onNextLine(builder.toString()); - builder.delete(0, builder.length() + 1); - } - else if (c != '\r') { - builder.append(c); - } - } - } - catch (IOException e) { - log.error(e.getMessage(), e); - } - } - finally { - if (client != null) { - client.close(); - } - } - } - }); - } - - private StatusCode awaitTermination(String host, Identifiable container) { - log.debug("Awaiting termination of container: {}", container.getId()); - - StatusCode status; - while (true) { - status = getStatus(host, container); - Integer statusCode = status.getStatusCode(); - if (statusCode != null) { - break; - } - - try { - Thread.sleep(1000); - } - catch (InterruptedException e) { - log.error(e.getMessage(), e); - } - } - - log.debug("Container: {} terminated with status: {}", container.getId(), status); - return status; - } - - private boolean isStopped(String host, Identifiable identifiable) { - try { - Map containers = getContainers(); - if (containers.containsKey(identifiable)) { - Container container = containers.get(identifiable); - return Strings.nullToEmpty(container.getStatus()).startsWith("Exit "); - } - return true; - } - catch (InternalServerErrorException | NotFoundException e) { - log.warn(e.getMessage(), e); - return true; - } - } - - private StatusCode getStatus(final String host, final Identifiable container) { - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Retrieving status of container: {}", container.getId()); - return client.target(host + "/containers/" + container.getId() + "/wait") - .request() - .accept(MediaType.APPLICATION_JSON) - .post(null, StatusCode.class); - } - finally { - if (client != null) { - client.close(); - } - } - } - - private boolean exists(String host, Identifiable container) { - try { - return getContainers().containsKey(container); - } - catch (InternalServerErrorException | NotFoundException e) { - return false; - } - } - - private void stopAndDelete(String host, Identifiable container) { - try { - log.debug("Attempting to stop container: {}", container); - do { - stop(host, container); - waitFor(1000); - } - while (!isStopped(host, container)); - } - catch (ClientErrorException e) { - log.warn(e.getMessage(), e); - } - - try { - log.debug("Attempting to delete container: {}", container); - do { - delete(host, container); - waitFor(1000); - } - while (exists(host, container)); - } - catch (ClientErrorException e) { - log.warn(e.getMessage(), e); - } - } - - private void stop(final String host, final Identifiable container) { - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Stopping container: {}", container.getId()); - client.target(host + "/containers/" + container.getId() + "/stop?t=5") - .request(MediaType.APPLICATION_JSON) - .accept(MediaType.TEXT_PLAIN) - .post(null); - } - finally { - if (client != null) { - client.close(); - } - } - } - - private void delete(String host, Identifiable container) { - Client client = null; - try { - client = ClientBuilder.newClient(); - log.debug("Removing container: {}", container.getId()); - client.target(host + "/containers/" + container.getId() + "?v=1&force=1") - .request() - .delete(); - } - finally { - if (client != null) { - client.close(); - } - } - } - - private void waitFor(int millis) { - try { - Thread.sleep(millis); - } - catch (InterruptedException e) { - log.warn(e.getMessage(), e); - } - } - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Error.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/Error.java deleted file mode 100644 index 0b3d145..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Error.java +++ /dev/null @@ -1,15 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import lombok.Data; - -@Data -class Error { - private String error; - private Error.Detail errorDetail; - - @Data - static class Detail { - private int code; - private String message; - } -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Identifiable.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/Identifiable.java deleted file mode 100644 index 785c29c..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Identifiable.java +++ /dev/null @@ -1,22 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import lombok.Data; -import lombok.EqualsAndHashCode; - -import com.fasterxml.jackson.annotation.JsonProperty; - -@Data -@EqualsAndHashCode(of = { "Id" }) -public class Identifiable { - @JsonProperty(required = false) - private String Id; - - @JsonProperty(required = false) - private String[] Warnings; - - @Override - public String toString() { - return Id.substring(0, 7); - } - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/ImageBuildObserver.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/ImageBuildObserver.java deleted file mode 100644 index bb088da..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/ImageBuildObserver.java +++ /dev/null @@ -1,11 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -public interface ImageBuildObserver { - - void onMessage(String message); - - void onError(String error); - - void onCompleted(); - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Logger.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/Logger.java deleted file mode 100644 index 1b4261d..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Logger.java +++ /dev/null @@ -1,13 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -public interface Logger { - - void initialize(Identifiable container); - - void onStart(); - - void onNextLine(String line); - - void onClose(int statusCode); - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/LxcConf.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/LxcConf.java deleted file mode 100644 index 3d9118a..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/LxcConf.java +++ /dev/null @@ -1,20 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -import com.fasterxml.jackson.annotation.JsonProperty; - -@Data -@AllArgsConstructor -@NoArgsConstructor -public class LxcConf { - - @JsonProperty("Key") - private String key; - - @JsonProperty("Value") - private String value; - -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/StatusCode.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/StatusCode.java deleted file mode 100644 index b0d4792..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/StatusCode.java +++ /dev/null @@ -1,11 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import lombok.Data; - -import com.fasterxml.jackson.annotation.JsonProperty; - -@Data -public class StatusCode { - @JsonProperty(required = false) - private Integer StatusCode; -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Stream.java b/build-server/src/main/java/nl/tudelft/ewi/build/docker/Stream.java deleted file mode 100644 index 6cbc7c6..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/docker/Stream.java +++ /dev/null @@ -1,8 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import lombok.Data; - -@Data -class Stream { - private String stream; -} \ No newline at end of file diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/BuildInstructionInterpreter.java b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/BuildInstructionInterpreter.java index d4edf6f..3f051d8 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/BuildInstructionInterpreter.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/BuildInstructionInterpreter.java @@ -1,10 +1,6 @@ package nl.tudelft.ewi.build.extensions.instructions; -import java.io.File; - -import nl.tudelft.ewi.build.docker.DefaultLogger; import nl.tudelft.ewi.build.jaxrs.models.BuildInstruction; -import nl.tudelft.ewi.build.jaxrs.models.BuildResult; public interface BuildInstructionInterpreter { @@ -12,6 +8,4 @@ public interface BuildInstructionInterpreter { String getCommand(T instruction); - BuildResult createResult(T instruction, DefaultLogger logger, File stagingDirectory); - } diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/DefaultBuildInstructionInterpreter.java b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/DefaultBuildInstructionInterpreter.java deleted file mode 100644 index 20ed531..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/DefaultBuildInstructionInterpreter.java +++ /dev/null @@ -1,30 +0,0 @@ -package nl.tudelft.ewi.build.extensions.instructions; - -import java.io.File; - -import nl.tudelft.ewi.build.docker.DefaultLogger; -import nl.tudelft.ewi.build.jaxrs.models.BuildInstruction; -import nl.tudelft.ewi.build.jaxrs.models.BuildResult; -import nl.tudelft.ewi.build.jaxrs.models.BuildResult.Status; - -public abstract class DefaultBuildInstructionInterpreter implements BuildInstructionInterpreter { - - @Override - public BuildResult createResult(T instruction, DefaultLogger logger, File stagingDirectory) { - BuildResult result = new BuildResult(); - setLogAndStatus(logger, result); - return result; - } - - protected void setLogAndStatus(DefaultLogger logger, BuildResult result) { - int exitCode = logger.getExitCode().get(); - Status status = Status.SUCCEEDED; - if (exitCode != 0) { - status = Status.FAILED; - } - - result.setStatus(status); - result.setLogLines(logger.getLogLines()); - } - -} diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/MavenBuildInstructionInterpreter.java b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/MavenBuildInstructionInterpreter.java index cabf7d6..a941ebd 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/MavenBuildInstructionInterpreter.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/instructions/MavenBuildInstructionInterpreter.java @@ -1,16 +1,13 @@ package nl.tudelft.ewi.build.extensions.instructions; -import java.io.File; import java.util.List; -import nl.tudelft.ewi.build.docker.DefaultLogger; -import nl.tudelft.ewi.build.jaxrs.models.MavenBuildResult; import nl.tudelft.ewi.build.jaxrs.models.MavenBuildInstruction; import com.google.common.base.Joiner; import com.google.common.collect.Lists; -public class MavenBuildInstructionInterpreter extends DefaultBuildInstructionInterpreter { +public class MavenBuildInstructionInterpreter implements BuildInstructionInterpreter { @Override public String getImage(MavenBuildInstruction instruction) { @@ -31,11 +28,4 @@ public String getCommand(MavenBuildInstruction instruction) { return Joiner.on(" ").join(partials); } - @Override - public MavenBuildResult createResult(MavenBuildInstruction instruction, DefaultLogger logger, File stagingDirectory) { - MavenBuildResult result = new MavenBuildResult(); - setLogAndStatus(logger, result); - return result; - } - } diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/GitStagingDirectoryPreparer.java b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/GitStagingDirectoryPreparer.java index f21e6af..1e269b0 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/GitStagingDirectoryPreparer.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/GitStagingDirectoryPreparer.java @@ -3,10 +3,10 @@ import java.io.File; import java.io.IOException; -import nl.tudelft.ewi.build.docker.Logger; - import lombok.extern.slf4j.Slf4j; +import nl.tudelft.ewi.build.builds.Logger; import nl.tudelft.ewi.build.jaxrs.models.GitSource; + import org.eclipse.jgit.api.CheckoutCommand; import org.eclipse.jgit.api.CloneCommand; import org.eclipse.jgit.api.Git; @@ -31,7 +31,7 @@ private Git cloneRepository(GitSource source, Logger logger, File stagingDirecto return clone.call(); } catch (GitAPIException e) { - logger.onNextLine("[FATAL] Failed to clone from repository: " + source.getRepositoryUrl()); + logger.println("[FATAL] Failed to clone from repository: " + source.getRepositoryUrl()); throw new IOException(e); } } @@ -58,7 +58,7 @@ private void checkoutCommit(GitSource source, Logger logger, Git git) throws IOE } } catch (GitAPIException e) { - logger.onNextLine("[FATAL] Failed to checkout to specified commit: " + source.getCommitId()); + logger.println("[FATAL] Failed to checkout to specified commit: " + source.getCommitId()); throw new IOException(e); } } diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/StagingDirectoryPreparer.java b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/StagingDirectoryPreparer.java index 3d8c24f..86fa4f0 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/StagingDirectoryPreparer.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/extensions/staging/StagingDirectoryPreparer.java @@ -3,8 +3,7 @@ import java.io.File; import java.io.IOException; -import nl.tudelft.ewi.build.docker.Logger; - +import nl.tudelft.ewi.build.builds.Logger; import nl.tudelft.ewi.build.jaxrs.models.Source; public interface StagingDirectoryPreparer { diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/BuildsResource.java b/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/BuildsResource.java index d6c2448..29bb01a 100644 --- a/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/BuildsResource.java +++ b/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/BuildsResource.java @@ -1,6 +1,8 @@ package nl.tudelft.ewi.build.jaxrs; import java.util.UUID; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import javax.inject.Inject; import javax.validation.Valid; @@ -10,14 +12,29 @@ import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.Invocation.Builder; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.Status; +import javax.ws.rs.core.Response.StatusType; + +import org.jboss.resteasy.util.Base64; + +import com.google.common.base.Strings; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; import lombok.extern.slf4j.Slf4j; + +import nl.tudelft.ewi.build.Config; import nl.tudelft.ewi.build.builds.BuildManager; +import nl.tudelft.ewi.build.builds.BuildManager.Build; import nl.tudelft.ewi.build.jaxrs.filters.RequireAuthentication; import nl.tudelft.ewi.build.jaxrs.models.BuildRequest; +import nl.tudelft.ewi.build.jaxrs.models.BuildResult; @Slf4j @Path("api/builds") @@ -26,31 +43,97 @@ public class BuildsResource { private final BuildManager manager; + private final ExecutorService executor = Executors + .newSingleThreadExecutor(); + private final Config config; @Inject - public BuildsResource(BuildManager manager) { + public BuildsResource(final BuildManager manager, final Config config) { this.manager = manager; + this.config = config; } @POST @RequireAuthentication - public Response onBuildRequest(@Valid BuildRequest buildRequest) { - UUID buildId = null; + public Response onBuildRequest(@Valid final BuildRequest buildRequest) { + Build build = null; try { - buildId = manager.schedule(buildRequest); - } - catch (Throwable e) { + build = manager.schedule(buildRequest); + Futures.addCallback(build, new FutureCallback() { + + @Override + public void onSuccess(BuildResult result) { + if (Strings.isNullOrEmpty(buildRequest.getCallbackUrl())) { + return; + } + + log.info("Returning build results to callback URL: {}", + buildRequest.getCallbackUrl()); + for (int i = 0; i <= 4; i++) { + Client client = ClientBuilder.newClient(); + try { + Response response = prepareCallback(client).post( + Entity.json(result)); + StatusType statusInfo = response.getStatusInfo(); + if (statusInfo.getStatusCode() >= 200 + && statusInfo.getStatusCode() < 300) { + log.info( + "Build result successfully returned to: {}", + buildRequest.getCallbackUrl()); + return; + } + log.warn( + "Could not return build result to: {}, status was: {} - {}", + buildRequest.getCallbackUrl(), + response.getStatus(), + statusInfo.getReasonPhrase()); + } catch (Throwable e) { + log.warn(e.getMessage(), e); + } finally { + if (client != null) { + client.close(); + } + } + + // Exponential backoff. + if (i < 4) { + try { + Thread.sleep(5000L * 2 ^ i); + } catch (InterruptedException e) { + } + } + } + + log.error("Could not return build result to: {}", + buildRequest.getCallbackUrl()); + } + + private Builder prepareCallback(Client client) { + String userPass = config.getClientId() + ":" + + config.getClientSecret(); + String authorization = "Basic " + + Base64.encodeBytes(userPass.getBytes()); + return client.target(buildRequest.getCallbackUrl()) + .request().header("Authorization", authorization); + } + + @Override + public void onFailure(Throwable t) { + // TODO Auto-generated method stub + + } + + }, executor); + } catch (Throwable e) { log.error(e.getMessage(), e); } - if (buildId == null) { + if (build == null) { return Response.status(Status.CONFLICT) - .entity("Server cannot accept build request.") - .build(); + .entity("Server cannot accept build request.").build(); } - return Response.ok(buildId) - .build(); + return Response.ok(build.getUUID()).build(); } @DELETE diff --git a/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/ImagesResource.java b/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/ImagesResource.java deleted file mode 100644 index bbabbd1..0000000 --- a/build-server/src/main/java/nl/tudelft/ewi/build/jaxrs/ImagesResource.java +++ /dev/null @@ -1,84 +0,0 @@ -package nl.tudelft.ewi.build.jaxrs; - -import javax.inject.Inject; -import javax.validation.Valid; -import javax.ws.rs.Consumes; -import javax.ws.rs.POST; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.StreamingOutput; - -import java.io.IOException; -import java.io.OutputStream; - -import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.docker.DockerManager; -import nl.tudelft.ewi.build.docker.ImageBuildObserver; -import nl.tudelft.ewi.build.jaxrs.filters.RequireAuthentication; -import nl.tudelft.ewi.build.jaxrs.models.ImageRequest; - -@Slf4j -@Path("api/images") -@Produces(MediaType.APPLICATION_JSON) -@Consumes(MediaType.APPLICATION_JSON) -public class ImagesResource { - - private final DockerManager manager; - - @Inject - public ImagesResource(DockerManager manager) { - this.manager = manager; - } - - @POST - @RequireAuthentication - public StreamingOutput onImageRequest(final @Valid ImageRequest imageRequest) { - return new StreamingOutput() { - @Override - public void write(final OutputStream output) throws IOException, WebApplicationException { - manager.buildImage(imageRequest.getName(), imageRequest.getInstructions(), new ImageBuildObserver() { - @Override - public void onMessage(String message) { - try { - if (!message.endsWith("\n")) { - message += "\n"; - } - output.write(message.getBytes()); - output.flush(); - } - catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - @Override - public void onError(String message) { - try { - if (message.endsWith("\n")) { - message = message.substring(0, message.length() - 1); - } - output.write(message.getBytes()); - output.flush(); - } - catch (IOException e) { - log.error(e.getMessage(), e); - } - } - - @Override - public void onCompleted() { - try { - output.close(); - } - catch (IOException e) { - log.error(e.getMessage(), e); - } - } - }); - } - }; - } - -} diff --git a/build-server/src/main/resources/config.properties b/build-server/src/main/resources/config.properties index 4738838..b458038 100644 --- a/build-server/src/main/resources/config.properties +++ b/build-server/src/main/resources/config.properties @@ -2,8 +2,7 @@ authorization.client-id = MichaelLaptop authorization.client-secret = t2hLCXVE docker.max-containers = 3 -docker.host = http://localhost:4243 -docker.staging-directory = /home/michael/workspace +docker.staging-directory = /workspace docker.working-directory = /workspace http.port = 8082 diff --git a/build-server/src/test/java/com/spotify/docker/client/MockedLogStream.java b/build-server/src/test/java/com/spotify/docker/client/MockedLogStream.java new file mode 100644 index 0000000..174b976 --- /dev/null +++ b/build-server/src/test/java/com/spotify/docker/client/MockedLogStream.java @@ -0,0 +1,36 @@ +package com.spotify.docker.client; + +import java.util.LinkedList; +import java.util.Queue; + +import org.apache.commons.io.input.NullInputStream; + +public class MockedLogStream extends LogStream { + + private final Queue logMessages; + + public MockedLogStream() { + super(new NullInputStream(0)); + this.logMessages = new LinkedList(); + } + + @Override + protected LogMessage computeNext() { + if (logMessages.isEmpty()) + return endOfData(); + return logMessages.remove(); + } + + public void addMessage(final LogMessage message) { + logMessages.add(message); + } + + @Override + public void close() { + try { + super.close(); + } + catch (Throwable t) {} + } + +} diff --git a/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java b/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java index 27f84d9..d12fb20 100644 --- a/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java +++ b/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java @@ -2,22 +2,43 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import java.util.UUID; +import java.util.concurrent.ExecutionException; +import lombok.extern.slf4j.Slf4j; import nl.tudelft.ewi.build.Config; -import nl.tudelft.ewi.build.docker.DockerManager; +import nl.tudelft.ewi.build.builds.BuildManager.Build; +import nl.tudelft.ewi.build.extensions.instructions.BuildInstructionInterpreterRegistry; +import nl.tudelft.ewi.build.extensions.staging.StagingDirectoryPreparerRegistry; import nl.tudelft.ewi.build.jaxrs.models.BuildRequest; +import nl.tudelft.ewi.build.jaxrs.models.BuildResult; +import nl.tudelft.ewi.build.jaxrs.models.BuildResult.Status; import nl.tudelft.ewi.build.jaxrs.models.GitSource; import nl.tudelft.ewi.build.jaxrs.models.MavenBuildInstruction; + +import org.hamcrest.Matchers; +import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; import org.mockito.runners.MockitoJUnitRunner; +import org.mockito.stubbing.Answer; + +import com.spotify.docker.client.DockerClient; +import com.spotify.docker.client.DockerClient.AttachParameter; +import com.spotify.docker.client.DockerException; +import com.spotify.docker.client.MockedLogStream; +import com.spotify.docker.client.messages.ContainerConfig; +import com.spotify.docker.client.messages.ContainerCreation; +import com.spotify.docker.client.messages.ContainerExit; +@Slf4j @RunWith(MockitoJUnitRunner.class) public class BuildManagerTest { @@ -25,17 +46,30 @@ public class BuildManagerTest { public static final int JOB_DURATION = 250; @Mock private Config config; - - private DockerManager dockerManager; + @Mock private DockerClient dockerClient; + private BuildManager manager; @Before - public void setUp() { + public void setUp() throws DockerException, InterruptedException { when(config.getMaximumConcurrentJobs()).thenReturn(CONCURRENT_JOBS); when(config.getWorkingDirectory()).thenReturn("/workspace"); + when(config.getStagingDirectory()).thenReturn("/workspace"); + + when(dockerClient.createContainer(Mockito.any(ContainerConfig.class), Mockito.anyString())) + .thenReturn(new ContainerCreation(UUID.randomUUID().toString())); + when(dockerClient.attachContainer(Mockito.anyString(), Mockito. anyVararg())) + .thenReturn(new MockedLogStream()); + when(dockerClient.waitContainer(Mockito.anyString())).thenReturn(new ContainerExit(0)); - dockerManager = new MockedDockerManager(CONCURRENT_JOBS, JOB_DURATION); - manager = new BuildManager(dockerManager, config); + manager = new BuildManager(config, dockerClient, + new StagingDirectoryPreparerRegistry(), + new BuildInstructionInterpreterRegistry()); + } + + @After + public void tearDown() { + manager.lifeCycleStopping(null); } @Test @@ -61,11 +95,13 @@ public void testThatJobIsRejectedWhenAtCapacity() throws InterruptedException { @Test public void testThatJobCanBeScheduledWhenCapacityIsRestored() throws InterruptedException { + UUID uuid = null; for (int i = 0; i < CONCURRENT_JOBS; i++) { - manager.schedule(createRequest()); + uuid = manager.schedule(createRequest()).getUUID(); } - - Thread.sleep(10000); + + assertNotNull(uuid); + manager.killBuild(uuid); assertNotNull(manager.schedule(createRequest())); } @@ -75,14 +111,41 @@ public void testThatJobCanBeScheduledWhenCapacityIsRestoredThroughTermination() manager.schedule(createRequest()); } - UUID scheduled = manager.schedule(createRequest()); + UUID scheduled = manager.schedule(createRequest()).getUUID(); assertNotNull(scheduled); - assertTrue(manager.killBuild(scheduled)); Thread.sleep(100); assertNotNull(scheduled); } + @Test + public void waitForABuild() throws InterruptedException, ExecutionException { + Build result = manager.schedule(createRequest()); + log.info("Result : {}", result.get()); + } + + @Test(timeout=2000) + public void testBuildWithTimeout() throws DockerException, InterruptedException, ExecutionException { + BuildRequest buildRequest = createRequest(); + buildRequest.setTimeout(1000); + + when(dockerClient.waitContainer(Mockito.anyString())).then(new Answer() { + + @Override + public ContainerExit answer(InvocationOnMock invocation) + throws Throwable { + Thread.sleep(20000l); + return new ContainerExit(0); + } + + }); + + Build future = manager.schedule(buildRequest); + BuildResult buildResult = future.get(); + Assert.assertEquals(Status.FAILED, buildResult.getStatus()); + Assert.assertThat(buildResult.getLogLines(), Matchers.hasItem("[FATAL] Build timed out!")); + } + private BuildRequest createRequest() { GitSource source = new GitSource(); source.setBranchName("master"); diff --git a/build-server/src/test/java/nl/tudelft/ewi/build/builds/MockedDockerManager.java b/build-server/src/test/java/nl/tudelft/ewi/build/builds/MockedDockerManager.java deleted file mode 100644 index 0889472..0000000 --- a/build-server/src/test/java/nl/tudelft/ewi/build/builds/MockedDockerManager.java +++ /dev/null @@ -1,101 +0,0 @@ -package nl.tudelft.ewi.build.builds; - -import javax.ws.rs.NotFoundException; - -import java.io.IOException; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import com.google.common.collect.Maps; -import com.google.common.util.concurrent.ListenableFuture; -import com.google.common.util.concurrent.ListeningExecutorService; -import com.google.common.util.concurrent.MoreExecutors; -import lombok.extern.slf4j.Slf4j; -import nl.tudelft.ewi.build.docker.ImageBuildObserver; -import nl.tudelft.ewi.build.docker.BuildReference; -import nl.tudelft.ewi.build.docker.DockerJob; -import nl.tudelft.ewi.build.docker.DockerManager; -import nl.tudelft.ewi.build.docker.Identifiable; -import nl.tudelft.ewi.build.docker.Logger; - -@Slf4j -public class MockedDockerManager implements DockerManager { - - private final ListeningExecutorService mainExecutor; - private final ExecutorService cleaningExecutor; - private final Map> futures; - - private final int jobDuration; - - public MockedDockerManager(int concurrentJobs, int jobDuration) { - this.mainExecutor = MoreExecutors.listeningDecorator(Executors.newScheduledThreadPool(concurrentJobs)); - this.cleaningExecutor = Executors.newScheduledThreadPool(concurrentJobs); - this.futures = Maps.newConcurrentMap(); - this.jobDuration = jobDuration; - } - - @Override - public BuildReference run(final Logger logger, final DockerJob job) { - final Identifiable containerId = new Identifiable(); - containerId.setId(UUID.randomUUID().toString()); - - ListenableFuture future = mainExecutor.submit(new Runnable() { - @Override - public void run() { - log.info("Started: {}", containerId); - logger.onStart(); - - long started = System.currentTimeMillis(); - while (started + jobDuration < System.currentTimeMillis()) { - logger.onNextLine("Writing to log..."); - try { - Thread.sleep(500); - } - catch (InterruptedException e) { - log.error(e.getMessage(), e); - } - } - - logger.onClose(0); - log.info("Terminated: {}", containerId); - } - }); - - log.info("Submitted job: {}", job); - futures.put(containerId, future); - Runnable cleaner = new Runnable() { - @Override - public void run() { - futures.remove(containerId); - log.info("Removed job: {}", job); - } - }; - - future.addListener(cleaner, cleaningExecutor); - - return new BuildReference(job, containerId, future); - } - - @Override - public void terminate(Identifiable containerId) { - ListenableFuture future = futures.get(containerId); - if (future != null) { - future.cancel(true); - } - else { - throw new NotFoundException("Container: " + containerId.getId() + " does not exist."); - } - } - - @Override - public int getActiveJobs() { - return futures.size(); - } - - @Override - public void buildImage(String name, String dockerFileContents, ImageBuildObserver observer) throws IOException { - } - -} diff --git a/build-server/src/test/java/nl/tudelft/ewi/build/docker/CommandParserTest.java b/build-server/src/test/java/nl/tudelft/ewi/build/docker/CommandParserTest.java deleted file mode 100644 index f7a7cb1..0000000 --- a/build-server/src/test/java/nl/tudelft/ewi/build/docker/CommandParserTest.java +++ /dev/null @@ -1,44 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import java.util.Arrays; -import java.util.Collection; -import java.util.List; - -import org.junit.Assert; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; -import org.junit.runners.Parameterized.Parameters; - -import com.google.common.collect.Lists; - -@RunWith(Parameterized.class) -public class CommandParserTest { - - @Parameters - public static Collection getParameters() { - return Arrays.asList(new Object[][] { - { "", Lists.newArrayList() }, - { "mvn", Lists.newArrayList("mvn") }, - { "mvn package", Lists.newArrayList("mvn", "package") }, - { "mvn package -DskipTests=true", Lists.newArrayList("mvn", "package", "-DskipTests=true") }, - { "echo \"hello\"", Lists.newArrayList("echo", "\"hello\"") }, - { "echo \"hello world\"", Lists.newArrayList("echo", "\"hello world\"") }, - }); - } - - private final String command; - private final List parts; - - public CommandParserTest(String command, List parts) { - this.command = command; - this.parts = parts; - } - - @Test - public void verifyParseCommand() { - List parsedCommand = CommandParser.parse(command); - Assert.assertEquals(parts, parsedCommand); - } - -} diff --git a/build-server/src/test/java/nl/tudelft/ewi/build/docker/DockerManagerImplTest.java b/build-server/src/test/java/nl/tudelft/ewi/build/docker/DockerManagerImplTest.java deleted file mode 100644 index 95378f0..0000000 --- a/build-server/src/test/java/nl/tudelft/ewi/build/docker/DockerManagerImplTest.java +++ /dev/null @@ -1,52 +0,0 @@ -package nl.tudelft.ewi.build.docker; - -import com.google.common.base.Joiner; -import nl.tudelft.ewi.build.SimpleConfig; -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Before; -import org.junit.Test; - -public class DockerManagerImplTest { - - private DockerManager manager; - - @Before - public void setUp() { - Assume.assumeTrue("Skipping Docker related tests. If these tests should run set VM argument: " - + "-Ddocker-tests.run=true", "true".equalsIgnoreCase(System.getProperty("docker-tests.run"))); - - SimpleConfig config = new SimpleConfig(); - this.manager = new DockerManagerImpl(config); - } - - @Test - public void testActiveJobsReturnsZeroWhenNoJobsScheduled() { - Assert.assertEquals(0, manager.getActiveJobs()); - } - - @Test - public void testStartingContainer() { - DefaultLogger logger = new DefaultLogger(); - DockerJob job = new DockerJob(); - job.setCommand("whoami"); - job.setImage("java-maven"); - - BuildReference build = manager.run(logger, job); - build.awaitTermination(); - - Assert.assertEquals("root", Joiner.on("").join(logger.getLogLines())); - } - - @Test - public void testTerminatingRunningContainer() throws InterruptedException { - DefaultLogger logger = new DefaultLogger(); - DockerJob job = new DockerJob(); - job.setCommand("sleep 100"); - job.setImage("java-maven"); - - BuildReference build = manager.run(logger, job); - Assert.assertTrue(build.terminate()); - } - -} diff --git a/pom.xml b/pom.xml index f860b03..2fed11b 100644 --- a/pom.xml +++ b/pom.xml @@ -21,17 +21,6 @@ - - org.jboss.resteasy - resteasy-client - ${resteasy.version} - - - slf4j-simple - org.slf4j - - - org.jboss.resteasy resteasy-guice @@ -112,7 +101,7 @@ org.projectlombok lombok - 1.12.2 + 1.14.8 com.google.guava From 85f12ff9323eb9e445b0e4b7cce7c35f1b2d3934 Mon Sep 17 00:00:00 2001 From: Jan-Willem Gmelig Meyling Date: Wed, 15 Apr 2015 19:38:39 +0200 Subject: [PATCH 2/2] Fixed failing test --- .../ewi/build/builds/BuildManagerTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java b/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java index d12fb20..4afae95 100644 --- a/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java +++ b/build-server/src/test/java/nl/tudelft/ewi/build/builds/BuildManagerTest.java @@ -4,9 +4,11 @@ import static org.junit.Assert.assertNull; import static org.mockito.Mockito.when; +import java.io.File; import java.util.UUID; import java.util.concurrent.ExecutionException; +import com.google.common.io.Files; import lombok.extern.slf4j.Slf4j; import nl.tudelft.ewi.build.Config; import nl.tudelft.ewi.build.builds.BuildManager.Build; @@ -20,8 +22,10 @@ import org.hamcrest.Matchers; import org.junit.After; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -50,11 +54,18 @@ public class BuildManagerTest { private BuildManager manager; + private static File stagingDirectory; + + @BeforeClass + public static void createTempDir() { + stagingDirectory = Files.createTempDir(); + stagingDirectory.deleteOnExit(); + } + @Before public void setUp() throws DockerException, InterruptedException { when(config.getMaximumConcurrentJobs()).thenReturn(CONCURRENT_JOBS); - when(config.getWorkingDirectory()).thenReturn("/workspace"); - when(config.getStagingDirectory()).thenReturn("/workspace"); + when(config.getStagingDirectory()).thenReturn(stagingDirectory.getAbsolutePath()); when(dockerClient.createContainer(Mockito.any(ContainerConfig.class), Mockito.anyString())) .thenReturn(new ContainerCreation(UUID.randomUUID().toString())); @@ -124,17 +135,17 @@ public void waitForABuild() throws InterruptedException, ExecutionException { log.info("Result : {}", result.get()); } - @Test(timeout=2000) + @Test(timeout=2000) // kill test after 2 seconds public void testBuildWithTimeout() throws DockerException, InterruptedException, ExecutionException { BuildRequest buildRequest = createRequest(); - buildRequest.setTimeout(1000); + buildRequest.setTimeout(1); // timeout 1 second when(dockerClient.waitContainer(Mockito.anyString())).then(new Answer() { @Override public ContainerExit answer(InvocationOnMock invocation) throws Throwable { - Thread.sleep(20000l); + Thread.sleep(20000l); // sleep 20 seconds return new ContainerExit(0); }