diff --git a/container/gradle.properties b/container/gradle.properties --- a/container/gradle.properties +++ b/container/gradle.properties @@ -1,2 +1,2 @@ group=org.implab.gradle -version=1.2.2 \ No newline at end of file +version=1.3.0 \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/ComposePlugin.java b/container/src/main/java/org/implab/gradle/containers/ComposePlugin.java --- a/container/src/main/java/org/implab/gradle/containers/ComposePlugin.java +++ b/container/src/main/java/org/implab/gradle/containers/ComposePlugin.java @@ -3,7 +3,6 @@ package org.implab.gradle.containers; import java.io.IOException; import java.util.HashMap; import java.util.Map; -import java.util.Optional; import org.gradle.api.DefaultTask; import org.gradle.api.Plugin; @@ -20,6 +19,8 @@ import org.implab.gradle.containers.task import org.implab.gradle.containers.tasks.WriteEnv; public abstract class ComposePlugin implements Plugin, ProjectMixin { + public static final String BUILD_GROUP = "build"; + public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages"; public final String COMPOSE_PROJECT_NAME = "COMPOSE_PROJECT_NAME"; @@ -59,7 +60,7 @@ public abstract class ComposePlugin impl var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class); var composeFile = containerExtension.getContextDirectory() - .file(composeExtension.getComposeFileName()); + .file(composeExtension.getComposeFileName()); var composeProfiles = composeExtension.getProfiles(); var cleanTask = task(CLEAN_TASK, Delete.class, t -> { @@ -88,6 +89,7 @@ public abstract class ComposePlugin impl }); var buildTask = task(BUILD_TASK, DefaultTask.class, t -> { + t.setGroup(BUILD_GROUP); t.dependsOn(writeEnvTask); }); diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java b/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java --- a/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java +++ b/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java @@ -5,8 +5,9 @@ import org.gradle.api.Project; import org.gradle.api.plugins.ExtraPropertiesExtension; import org.implab.gradle.containers.tasks.BuildImage; import org.implab.gradle.containers.tasks.PushImage; -import org.implab.gradle.containers.tasks.RunImage; +import org.implab.gradle.containers.tasks.RunContainer; import org.implab.gradle.containers.tasks.SaveImage; +import org.implab.gradle.containers.tasks.StopContainer; import org.implab.gradle.containers.tasks.TagImage; public class ContainerBasePlugin implements Plugin { @@ -47,7 +48,8 @@ public class ContainerBasePlugin impleme exportClasses( project, - BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunImage.class); + BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunContainer.class, + StopContainer.class); } diff --git a/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java b/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java --- a/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java @@ -2,11 +2,10 @@ package org.implab.gradle.containers.cli import java.io.File; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutionException; import org.gradle.api.logging.Logger; @@ -30,104 +29,93 @@ public abstract class DockerTraits { public abstract String getCliCmd(); - Process startProcess(ProcessBuilder builder) throws IOException { - getLogger().info("Starting: {}", builder.command()); - return builder.start(); - } - - protected boolean checkRetCode(Process proc, int code) throws InterruptedException { + protected boolean checkRetCode(ProcessSpec proc, int code) + throws InterruptedException, ExecutionException, IOException { if (getLogger().isInfoEnabled()) { - Utils.redirectIO(proc.getInputStream(), getLogger()::info); - Utils.redirectIO(proc.getErrorStream(), getLogger()::info); + proc.redirectStdout(RedirectTo.consumer(getLogger()::info)) + .redirectStderr(RedirectTo.consumer(getLogger()::info)); } - return proc.waitFor() == code; + getLogger().info("Starting: {}", proc.command()); + + return proc.start().get() == code; } - protected void complete(Process proc) throws InterruptedException, IOException { + protected void exec(ProcessSpec proc) throws InterruptedException, IOException, ExecutionException { if (getLogger().isInfoEnabled()) - Utils.redirectIO(proc.getInputStream(), getLogger()::info); + proc.redirectStdout(RedirectTo.consumer(getLogger()::info)); if (getLogger().isErrorEnabled()) - Utils.redirectIO(proc.getErrorStream(), getLogger()::error); + proc.redirectStderr(RedirectTo.consumer(getLogger()::error)); - var code = proc.waitFor(); + getLogger().info("Starting: {}", proc.command()); + + var code = proc.start().get(); if (code != 0) { getLogger().error("The process exited with code {}", code); throw new IOException("The process exited with error code " + code); } } - protected ProcessBuilder builder(String... args) { - var argsList = new ArrayList(args.length + 1); - Arrays.stream(args).forEach(argsList::add); - - return builder(argsList); - } + protected ProcessSpec builder(String... args) { + var spec = new ProcessSpec().args(getCliCmd()).args(args); - protected ProcessBuilder builder(List args) { - var command = new ArrayList(args.size() + 1); + getWorkingDir().ifPresent(spec::directory); - command.add(getCliCmd()); - args.forEach(command::add); - - var builder = new ProcessBuilder(command); - - getWorkingDir().ifPresent(builder::directory); - return builder; + return spec; } public void buildImage(String imageName, File contextDirectory, List options) - throws IOException, InterruptedException { - var args = new ArrayList(); - args.add(BUILD_COMMAND); - args.addAll(options); - args.add("-t"); - args.add(imageName); - args.add(contextDirectory.getAbsolutePath()); - complete(startProcess(builder(args))); + throws IOException, InterruptedException, ExecutionException { + var spec = builder(BUILD_COMMAND) + .args(options) + .args("-t", imageName, contextDirectory.getAbsolutePath()); + + exec(spec); } - public void pushImage(String image, List options) throws InterruptedException, IOException { - var args = new ArrayList(); - args.add(PUSH_COMMAND); - args.addAll(options); - args.add(image); - complete(startProcess(builder(args))); + public void pushImage(String image, List options) + throws InterruptedException, IOException, ExecutionException { + var spec = builder(PUSH_COMMAND) + .args(options) + .args(image); + + exec(spec); } - public void runImage(String image, List options, List command) + public ProcessSpec runImage(String image, List options, List command) throws InterruptedException, IOException { - var args = new ArrayList(); - args.add(RUN_COMMAND); - args.addAll(options); - args.add(image); - args.addAll(command); - complete(startProcess(builder(args))); + return builder(RUN_COMMAND) + .args(options) + .args(image) + .args(command); } - public void saveImage(Set images, File output) throws InterruptedException, IOException { + public void stopContainer(String containerId, List options) throws InterruptedException, IOException, ExecutionException { + exec(builder(STOP_COMMAND, containerId).args(options)); + } + + public void saveImage(Set images, File output) + throws InterruptedException, IOException, ExecutionException { if (output.exists()) output.delete(); - var args = new ArrayList(); - args.add(SAVE_COMMAND); - args.add("-o"); - args.add(output.getAbsolutePath()); - images.forEach(args::add); + var spec = builder(SAVE_COMMAND) + .args("-o", output.getAbsolutePath()) + .args(images); - complete(startProcess(builder(args))); + exec(spec); } - public void tagImage(String source, String target) throws InterruptedException, IOException { - complete(startProcess(builder(TAG_COMMAND, source, target))); + public void tagImage(String source, String target) throws InterruptedException, IOException, ExecutionException { + exec(builder(TAG_COMMAND, source, target)); } - public boolean imageExists(String imageId) throws InterruptedException, IOException { + public boolean imageExists(String imageId) throws InterruptedException, IOException, ExecutionException { getLogger().info("Check image {} exists", imageId); return checkRetCode( - startProcess(builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId)), + builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId), 0); } @@ -136,7 +124,7 @@ public abstract class DockerTraits { try { var imageId = Utils.readImageRef(imageIdFile); return imageExists(imageId.getId()); - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | ExecutionException e) { getLogger().error("Failed to read imageId {}: {}", imageIdFile, e); return false; } @@ -144,43 +132,31 @@ public abstract class DockerTraits { return false; } - List composeArgs(File primaryCompose, Set profiles, String... extra) { - var args = new ArrayList(); - - args.add(COMPOSE_COMMAND); - args.add("--file"); - args.add(primaryCompose.getAbsolutePath()); + ProcessSpec compose(File primaryCompose, Set profiles, String... extra) { + var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath()); - if (profiles.size() > 0) { - for (var profile : profiles) { - args.add("--profile"); - args.add(profile); - } - } + for (var profile : profiles) + spec.args("--profile", profile); - args.addAll(List.of(extra)); + spec.args(extra); - return args; + return spec; } - public void composeUp(File primaryCompose, Set profiles) throws InterruptedException, IOException { - var args = composeArgs(primaryCompose, profiles, UP_COMMAND, "--detach"); - - complete(startProcess(builder(args))); + public void composeUp(File primaryCompose, Set profiles) throws InterruptedException, IOException, ExecutionException { + exec(compose(primaryCompose, profiles, UP_COMMAND, "--detach")); } - public void composeStop(File primaryCompose, Set profiles) throws InterruptedException, IOException { - var args = composeArgs(primaryCompose, profiles, STOP_COMMAND); - complete(startProcess(builder(args))); + public void composeStop(File primaryCompose, Set profiles) throws InterruptedException, IOException, ExecutionException { + exec(compose(primaryCompose, profiles, STOP_COMMAND)); } public void composeRm(File primaryCompose, Set profiles, boolean removeVolumes) - throws InterruptedException, IOException { - var args = composeArgs(primaryCompose, profiles, RM_COMMAND, "--force", "--stop"); - + throws InterruptedException, IOException, ExecutionException { + var spec = compose(primaryCompose, profiles, RM_COMMAND, "--force", "--stop"); if (removeVolumes) - args.add("--volumes"); + spec.args("--volumes"); - complete(startProcess(builder(args))); + exec(spec); } } diff --git a/container/src/main/java/org/implab/gradle/containers/cli/ProcessSpec.java b/container/src/main/java/org/implab/gradle/containers/cli/ProcessSpec.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/ProcessSpec.java @@ -0,0 +1,106 @@ +package org.implab.gradle.containers.cli; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; + +public class ProcessSpec { + private final ProcessBuilder builder; + + private RedirectFrom inputRedirect; + + private RedirectTo outputRedirect; + + private RedirectTo errorRedirect; + + private List command = new ArrayList<>(); + + private File directory; + + public ProcessSpec() { + builder = new ProcessBuilder(); + } + + public CompletableFuture start() throws IOException { + var tasks = new ArrayList>(); + + builder.command(this.command); + builder.directory(directory); + + var proc = builder.start(); + + tasks.add(proc.onExit()); + + if (inputRedirect != null) + tasks.add(inputRedirect.redirect(proc.getOutputStream())); + + if (outputRedirect != null) + tasks.add(outputRedirect.redirect(proc.getInputStream())); + + if (errorRedirect != null) + tasks.add(errorRedirect.redirect(proc.getErrorStream())); + + return CompletableFuture + .allOf(tasks.toArray(new CompletableFuture[0])) + .thenApply(t -> proc.exitValue()); + } + + public void exec() throws InterruptedException, ExecutionException, IOException { + var code = start().get(); + if (code != 0) + throw new IOException("The process exited with error code " + code); + } + + public List command() { + return this.command; + } + + public ProcessSpec command(String... args) { + this.command = new ArrayList<>(); + args(args); + return this; + } + + public ProcessSpec directory(File workingDir) { + this.directory = workingDir; + return this; + } + + public ProcessSpec command(Collection args) { + this.command = new ArrayList<>(); + args(args); + return this; + } + + public ProcessSpec args(String... args) { + for (String arg : args) + this.command.add(arg); + + return this; + } + + public ProcessSpec args(Collection args) { + this.command.addAll(args); + + return this; + } + + public ProcessSpec redirectStderr(RedirectTo to) { + this.errorRedirect = to; + return this; + } + + public ProcessSpec redirectStdin(RedirectFrom from) { + this.inputRedirect = from; + return this; + } + + public ProcessSpec redirectStdout(RedirectTo to) { + this.outputRedirect = to; + return this; + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/cli/RedirectFrom.java b/container/src/main/java/org/implab/gradle/containers/cli/RedirectFrom.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/RedirectFrom.java @@ -0,0 +1,32 @@ +package org.implab.gradle.containers.cli; + +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; + +public interface RedirectFrom { + CompletableFuture redirect(OutputStream to); + + public static RedirectFrom file(final File file) { + return to -> CompletableFuture.runAsync(() -> { + try (var from = new FileInputStream(file); to) { + from.transferTo(to); + } catch (Exception e) { + // silence! + } + }); + } + + public static RedirectFrom stream(final InputStream from) { + return to -> CompletableFuture.runAsync(() -> { + try (from; to) { + from.transferTo(to); + } catch (Exception e) { + // silence! + } + }); + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/cli/RedirectTo.java b/container/src/main/java/org/implab/gradle/containers/cli/RedirectTo.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/RedirectTo.java @@ -0,0 +1,47 @@ +package org.implab.gradle.containers.cli; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Scanner; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; + +/** + * RedirectSpec + */ +public interface RedirectTo { + CompletableFuture redirect(InputStream from); + + public static RedirectTo consumer(final Consumer consumer) { + + return (src) -> CompletableFuture.runAsync(() -> { + try (Scanner sc = new Scanner(src)) { + while (sc.hasNextLine()) { + consumer.accept(sc.nextLine()); + } + } + }); + } + + public static RedirectTo file(final File file) { + return src -> CompletableFuture.runAsync(() -> { + try (OutputStream out = new FileOutputStream(file)) { + src.transferTo(out); + } catch (Exception e) { + // silence! + } + }); + } + + public static RedirectTo stream(final OutputStream dest) { + return src -> CompletableFuture.runAsync(() -> { + try (dest; src) { + src.transferTo(dest); + } catch (Exception e) { + // silence! + } + }); + } +} \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/cli/Utils.java b/container/src/main/java/org/implab/gradle/containers/cli/Utils.java --- a/container/src/main/java/org/implab/gradle/containers/cli/Utils.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/Utils.java @@ -11,6 +11,9 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import java.util.stream.StreamSupport; import java.nio.file.Files; import java.util.List; @@ -29,24 +32,34 @@ import groovy.json.JsonGenerator.Convert import groovy.lang.Closure; public final class Utils { - public static void redirectIO(final InputStream src, final Action consumer) { - new Thread(() -> { + public static CompletableFuture redirectIO(final InputStream src, final Action consumer) { + return CompletableFuture.runAsync(() -> { try (Scanner sc = new Scanner(src)) { while (sc.hasNextLine()) { consumer.execute(sc.nextLine()); } } - }).start(); + }); } - public static void redirectIO(final InputStream src, final File file) { - new Thread(() -> { + public static CompletableFuture redirectIO(final InputStream src, final File file) { + return CompletableFuture.runAsync(() -> { try (OutputStream out = new FileOutputStream(file)) { src.transferTo(out); } catch (Exception e) { // silence! } - }).start(); + }); + } + + public static CompletableFuture redirectIO(final InputStream src, final OutputStream dst) { + return CompletableFuture.runAsync(() -> { + try (dst) { + src.transferTo(dst); + } catch (Exception e) { + // silence! + } + }); } public static void closeSilent(Closeable handle) { @@ -136,4 +149,8 @@ public final class Utils { }; } + public static Set mapToString(Set set) { + return set.stream().map(Object::toString).collect(Collectors.toSet()); + } + } \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/RedirectFromSpec.java b/container/src/main/java/org/implab/gradle/containers/dsl/RedirectFromSpec.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/dsl/RedirectFromSpec.java @@ -0,0 +1,37 @@ +package org.implab.gradle.containers.dsl; + +import java.io.File; +import java.io.InputStream; +import java.util.Optional; +import java.util.function.Supplier; + +import org.gradle.api.provider.Provider; +import org.implab.gradle.containers.cli.RedirectFrom; + +public class RedirectFromSpec { + private Supplier streamRedirect; + + public boolean isRedirected() { + return streamRedirect != null; + } + + public Optional getRedirection() { + return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty(); + } + + public void fromFile(File file) { + this.streamRedirect = () -> RedirectFrom.file(file); + } + + public void fromFile(Provider file) { + this.streamRedirect = file.map(RedirectFrom::file)::get; + } + + public void fromStream(InputStream stream) { + this.streamRedirect = () -> RedirectFrom.stream(stream); + } + + public void fromStream(Provider stream) { + this.streamRedirect = stream.map(RedirectFrom::stream)::get; + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/RedirectToSpec.java b/container/src/main/java/org/implab/gradle/containers/dsl/RedirectToSpec.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/dsl/RedirectToSpec.java @@ -0,0 +1,42 @@ +package org.implab.gradle.containers.dsl; + +import java.io.File; +import java.io.OutputStream; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.gradle.api.provider.Provider; +import org.implab.gradle.containers.cli.RedirectTo; + +public class RedirectToSpec { + private Supplier streamRedirect; + + public boolean isRedirected() { + return streamRedirect != null; + } + + public Optional getRedirection() { + return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty(); + } + + public void toFile(File file) { + this.streamRedirect = () -> RedirectTo.file(file); + } + + public void toFile(Provider file) { + this.streamRedirect = file.map(RedirectTo::file)::get; + } + + public void toStream(OutputStream stream) { + this.streamRedirect = () -> RedirectTo.stream(stream); + } + + public void toStream(Provider stream) { + this.streamRedirect = stream.map(RedirectTo::stream)::get; + } + + public void toConsumer(Consumer consumer) { + this.streamRedirect = () -> RedirectTo.consumer(consumer); + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java @@ -43,7 +43,7 @@ public abstract class BuildImage extends public abstract Property getBuildTarget(); @Input - public abstract Property getImageName(); + public abstract Property getImageName(); @Internal public abstract RegularFileProperty getImageIdFile(); @@ -101,7 +101,7 @@ public abstract class BuildImage extends // add extra parameters getOptions().get().forEach(args::add); - var imageTag = getImageName().get(); + var imageTag = getImageName().map(Object::toString).get(); // build image docker().buildImage( diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeExec.java b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeExec.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeExec.java @@ -0,0 +1,10 @@ +package org.implab.gradle.containers.tasks; + +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Internal; + +public abstract class ComposeExec { + + @Internal + public abstract Property getServiceName(); +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeRm.java b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeRm.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeRm.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeRm.java @@ -1,12 +1,13 @@ package org.implab.gradle.containers.tasks; import java.io.IOException; +import java.util.concurrent.ExecutionException; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; -public abstract class ComposeRm extends ComposeStop { +public abstract class ComposeRm extends ComposeTask { @Internal public abstract Property getRemoveVolumes(); @@ -16,7 +17,7 @@ public abstract class ComposeRm extends } @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { docker().composeRm(getComposeFile().get().getAsFile(), getProfiles().get(), getRemoveVolumes().get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeStop.java b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeStop.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeStop.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeStop.java @@ -1,13 +1,14 @@ package org.implab.gradle.containers.tasks; import java.io.IOException; +import java.util.concurrent.ExecutionException; import org.gradle.api.tasks.TaskAction; public abstract class ComposeStop extends ComposeTask { @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { docker().composeStop(getComposeFile().getAsFile().get(), getProfiles().get()); } } \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeUp.java b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeUp.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeUp.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeUp.java @@ -1,13 +1,14 @@ package org.implab.gradle.containers.tasks; import java.io.IOException; +import java.util.concurrent.ExecutionException; import org.gradle.api.tasks.TaskAction; public abstract class ComposeUp extends ComposeTask { @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { docker().composeUp(getComposeFile().getAsFile().get(), getProfiles().get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java b/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java @@ -11,6 +11,9 @@ import org.gradle.api.tasks.Internal; import org.implab.gradle.containers.ContainerExtension; import org.implab.gradle.containers.PropertiesMixin; import org.implab.gradle.containers.cli.DockerTraits; +import org.implab.gradle.containers.cli.ProcessSpec; +import org.implab.gradle.containers.cli.RedirectFrom; +import org.implab.gradle.containers.cli.RedirectTo; public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin { @@ -33,11 +36,36 @@ public abstract class DockerCliTask exte getCliCmd().convention(getContainerExtension().getCliCmd()); } - + + protected Optional loggerInfoRedirect() { + return getLogger().isInfoEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::info)) : Optional.empty(); + } + + protected Optional loggerErrorRedirect() { + return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty(); + } + + protected Optional getStdoutRedirection() { + return loggerInfoRedirect(); + } + + protected Optional getStderrRedirection() { + return loggerErrorRedirect(); + } + + protected Optional getStdinRedirection() { + return Optional.empty(); + } + protected DockerTraits docker() { return new TaskDockerTraits(); } + protected void exec(ProcessSpec spec) { + + getLogger().info("Starting: {}", spec.command()); + } + class TaskDockerTraits extends DockerTraits { @Override @@ -48,8 +76,8 @@ public abstract class DockerCliTask exte @Override public Optional getWorkingDir() { return getWorkingDirectory() - .map(Optional::of) - .getOrElse(Optional.empty()); + .map(Optional::of) + .getOrElse(Optional.empty()); } @Override diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java @@ -1,6 +1,7 @@ package org.implab.gradle.containers.tasks; import java.io.IOException; +import java.util.concurrent.ExecutionException; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; @@ -12,21 +13,21 @@ import org.implab.gradle.containers.dsl. public abstract class PushImage extends DockerCliTask implements OptionsMixin { @Input - public abstract Property getImageName(); + public abstract Property getImageName(); @Input @Optional - public abstract Property getTransport(); + public abstract Property getTransport(); @Input @Optional public abstract ListProperty getOptions(); @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { docker().pushImage( - getImageName().get(), + getImageName().map(Object::toString).get(), getOptions().get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java rename from container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java rename to container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java @@ -1,24 +1,94 @@ package org.implab.gradle.containers.tasks; +import java.io.File; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.ExecutionException; + import org.gradle.api.Action; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.RedirectFrom; +import org.implab.gradle.containers.cli.RedirectTo; import org.implab.gradle.containers.dsl.OptionsMixin; +import org.implab.gradle.containers.dsl.RedirectFromSpec; +import org.implab.gradle.containers.dsl.RedirectToSpec; import org.implab.gradle.containers.dsl.VolumeSpec; -public abstract class RunImage extends DockerCliTask implements OptionsMixin { +public abstract class RunContainer extends DockerCliTask implements OptionsMixin { + + private final RedirectToSpec redirectStderr = new RedirectToSpec(); + + private final RedirectToSpec redirectStdout = new RedirectToSpec(); + + private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); + + private boolean transientContainer = true; + + private boolean detached = false; + + private boolean interactive = false; + + @Internal + public abstract Property getImageName(); - @Input - public abstract Property getImageName(); + @Internal + public abstract ListProperty getCommandLine(); + + @Internal + public RedirectFromSpec getStdin() { + return redirectStdin; + } + + @Internal + public RedirectToSpec getStdout() { + return redirectStdout; + } + + @Internal + public RedirectToSpec getStderr() { + return redirectStderr; + } - @Input - @Optional - public abstract ListProperty getCommandLine(); - + @Internal + public boolean isTransientContainer() { + return transientContainer; + } + + public void setTransientContainer(boolean value) { + transientContainer = value; + } + + @Internal + public boolean isDetached() { + // if IO was redirected the container should run in foreground + if (redirectStdin.isRedirected() || redirectStdout.isRedirected() || redirectStderr.isRedirected()) + return false; + + return detached; + } + + public void setDetached(boolean value) { + detached = value; + } + + @Internal + public boolean isInteractive() { + // enable interactive mode when processing standard input + return redirectStdin.isRedirected() || interactive; + } + + @Internal + public abstract RegularFileProperty getContainerIdFile(); + + public RunContainer() { + getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid")); + } public void volume(Action configure) { getOptions().add("-v"); @@ -29,16 +99,61 @@ public abstract class RunImage extends D })); } - void commandLine(String ...args) { + void commandLine(String... args) { getCommandLine().addAll(args); } + @Override + protected Optional getStdoutRedirection() { + return redirectStdout.getRedirection().or(super::getStdoutRedirection); + } + + @Override + protected Optional getStderrRedirection() { + return redirectStderr.getRedirection().or(super::getStderrRedirection); + } + + @Override + protected Optional getStdinRedirection() { + return redirectStdin.getRedirection(); + } + @TaskAction - public void run() throws InterruptedException, IOException { - docker().runImage( - getImageName().get(), - getOptions().get(), + public void run() throws InterruptedException, IOException, ExecutionException { + var params = new ArrayList<>(getOptions().get()); + + if (isInteractive()) + params.add("--interactive"); + if (isTransientContainer()) + params.add("--rm"); + if (isDetached()) + params.add("--detach"); + if (getContainerIdFile().isPresent()) { + getContainerIdFile().getAsFile().get().delete(); + + params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString())); + } + + var spec = docker().runImage( + getImageName().map(Object::toString).get(), + params, getCommandLine().get()); + + // if process will run in foreground, then setup default redirects + redirectStdout.getRedirection() + .or(this::loggerInfoRedirect) + .ifPresent(spec::redirectStdout); + + redirectStderr.getRedirection() + .or(this::loggerErrorRedirect) + .ifPresent(spec::redirectStderr); + + redirectStdin.getRedirection().ifPresent(spec::redirectStdin); + + getLogger().info("Staring: {}", spec.command()); + + // runs the command and checks the error code + spec.exec(); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java @@ -4,6 +4,7 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.FileCollection; @@ -21,7 +22,7 @@ import org.implab.gradle.containers.cli. public abstract class SaveImage extends DockerCliTask { @Input - public abstract SetProperty getExportImages(); + public abstract SetProperty getExportImages(); @OutputFile public Provider getArchiveFile() { @@ -103,9 +104,9 @@ public abstract class SaveImage extends } @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { docker().saveImage( - getExportImages().get(), + getExportImages().map(Utils::mapToString).get(), getArchiveFile().map(RegularFile::getAsFile).get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/StopContainer.java b/container/src/main/java/org/implab/gradle/containers/tasks/StopContainer.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/StopContainer.java @@ -0,0 +1,66 @@ +package org.implab.gradle.containers.tasks; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFile; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.Utils; + +public abstract class StopContainer extends DockerCliTask { + + @Internal + public abstract RegularFileProperty getContainerIdFile(); + + @Internal + public abstract Property getContainerName(); + + @Internal + public abstract Property getStopSignal(); + + @Internal + public abstract Property getStopTimeout(); + + Provider readCid(RegularFile regularFile) { + var file = regularFile.getAsFile(); + + return provider(() -> { + try { + return file.isFile() ? Utils.readAll(file) : null; + } catch (IOException e) { + throw new GradleException("Failed to read container id from file " + file.toString(), e); + } + }); + } + + @TaskAction + public void run() throws InterruptedException, IOException, ExecutionException { + var cid = getContainerIdFile().flatMap(this::readCid).orElse(getContainerName()); + + if (!cid.isPresent()) { + getLogger().info("The container name or id hasn't been specified"); + return; + } + + var options = new ArrayList(); + + if (getStopSignal().isPresent()) + options.addAll(List.of("--signal", getStopSignal().get())); + + if (getStopTimeout().isPresent()) + options.addAll(List.of("--time", getStopTimeout().map(Object::toString).get())); + + docker().stopContainer(cid.get(), options); + + if (getContainerIdFile().isPresent()) + getContainerIdFile().getAsFile().get().delete(); + } + +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java @@ -1,28 +1,29 @@ package org.implab.gradle.containers.tasks; import java.io.IOException; -import java.util.HashSet; import java.util.Set; +import java.util.concurrent.ExecutionException; import org.gradle.api.provider.Property; import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.Utils; public abstract class TagImage extends DockerCliTask { @Input - public abstract Property getSrcImage(); + public abstract Property getSrcImage(); @Input - public abstract SetProperty getTags(); + public abstract SetProperty getTags(); @Input @Deprecated - public abstract Property getDestImage(); + public abstract Property getDestImage(); private Set getImageTags() { - var tags = new HashSet<>(getTags().get()); - tags.add(getDestImage().get()); + var tags = getTags().map(Utils::mapToString).get(); + tags.add(getDestImage().map(Object::toString).get()); return tags; } @@ -31,9 +32,9 @@ public abstract class TagImage extends D } @TaskAction - public void run() throws InterruptedException, IOException { + public void run() throws InterruptedException, IOException, ExecutionException { var tags = getImageTags(); - var src = getSrcImage().get(); + var src = getSrcImage().map(Object::toString).get(); if (tags.size() == 0) getLogger().info("No tags were specified");