diff --git a/container/src/main/java/org/implab/gradle/containers/ComposeExtension.java b/container/src/main/java/org/implab/gradle/containers/ComposeExtension.java --- a/container/src/main/java/org/implab/gradle/containers/ComposeExtension.java +++ b/container/src/main/java/org/implab/gradle/containers/ComposeExtension.java @@ -1,6 +1,12 @@ package org.implab.gradle.containers; +import javax.inject.Inject; + +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.ProjectLayout; +import org.gradle.api.file.RegularFile; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.provider.SetProperty; public abstract class ComposeExtension { @@ -8,9 +14,17 @@ public abstract class ComposeExtension { public abstract SetProperty getProfiles(); + public abstract DirectoryProperty getContextDirectory(); + public abstract Property getComposeFileName(); - public ComposeExtension() { + public Provider getComposeFile() { + return getContextDirectory().file(getComposeFileName()); + } + + @Inject + public ComposeExtension(ProjectLayout layout) { + getContextDirectory().convention(layout.getBuildDirectory().dir("context")); getComposeFileName().convention(COMPOSE_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 @@ -54,30 +54,25 @@ public abstract class ComposePlugin impl var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE); // basic configuration, register extension - var basePlugin = plugin(ContainerBasePlugin.class); - var containerExtension = basePlugin.getContainerExtension(); + plugin(ContainerBasePlugin.class); var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class); - var composeFile = containerExtension.getContextDirectory() - .file(composeExtension.getComposeFileName()); - var composeProfiles = composeExtension.getProfiles(); - var cleanTask = task(CLEAN_TASK, Delete.class, t -> { - t.delete(containerExtension.getContextDirectory()); + t.delete(composeExtension.getContextDirectory()); }); // copy task from src/main var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> { t.mustRunAfter(cleanTask); t.from(projectDirectory().dir("src/main")); - t.into(containerExtension.getContextDirectory()); + t.into(composeExtension.getContextDirectory()); }); // write .env var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> { t.dependsOn(processResources, containerImages); - t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME)); + t.getEnvFile().set(composeExtension.getContextDirectory().file(ENV_FILE_NAME)); var group = project.getGroup(); if (group != null && group.toString().length() > 0) { @@ -97,16 +92,12 @@ public abstract class ComposePlugin impl // stop must run after build t.mustRunAfter(buildTask); - t.getProfiles().addAll(composeProfiles); - t.getComposeFile().set(composeFile); }); var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> { // rm must run after build and stop t.mustRunAfter(buildTask, stopTask); - t.getProfiles().addAll(composeProfiles); - t.getComposeFile().set(composeFile); }); task(COMPOSE_UP_TASK, ComposeUp.class, t -> { @@ -114,8 +105,6 @@ public abstract class ComposePlugin impl // up must run after stop and rm t.mustRunAfter(stopTask, rmTask); - t.getProfiles().addAll(composeProfiles); - t.getComposeFile().set(composeFile); }); } 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 @@ -4,13 +4,19 @@ import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.plugins.ExtraPropertiesExtension; import org.implab.gradle.containers.tasks.BuildImage; +import org.implab.gradle.containers.tasks.ComposeExec; +import org.implab.gradle.containers.tasks.ComposeRm; +import org.implab.gradle.containers.tasks.ComposeStop; +import org.implab.gradle.containers.tasks.ComposeUp; +import org.implab.gradle.containers.tasks.ExecContainer; import org.implab.gradle.containers.tasks.PushImage; +import org.implab.gradle.containers.tasks.RmContainer; 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 { +public abstract class ContainerBasePlugin implements Plugin, ProjectMixin { public static final String CONTAINER_EXTENSION_NAME = "container"; private ContainerExtension containerExtension; @@ -30,26 +36,13 @@ public class ContainerBasePlugin impleme @Override public void apply(Project project) { - containerExtension = project.getObjects().newInstance(ContainerExtension.class); - - // TODO: move properties initialization into the constructor - containerExtension.getImageAuthority() - .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority"))); - - containerExtension.getImageGroup() - .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup"))); - - containerExtension.getCliCmd() - .convention(project - .provider(() -> (String) project.getProperties().get("containerCli")) - .orElse("docker")); - - project.getExtensions().add(CONTAINER_EXTENSION_NAME, containerExtension); + containerExtension = extension(CONTAINER_EXTENSION_NAME, ContainerExtension.class); exportClasses( project, BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunContainer.class, - StopContainer.class); + StopContainer.class, ExecContainer.class, RmContainer.class, ComposeUp.class, ComposeExec.class, + ComposeRm.class, ComposeStop.class); } diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java b/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java --- a/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java +++ b/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java @@ -61,6 +61,16 @@ public abstract class ContainerExtension getImageTag().set(provider(() -> Optional.ofNullable(project.getVersion()) .map(Object::toString) .orElse("latest"))); + + getImageAuthority() + .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority"))); + + getImageGroup() + .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup"))); + + getCliCmd().convention(project + .provider(() -> (String) project.getProperties().get("containerCli")) + .orElse("docker")); } ImageName createImageName() { diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java b/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java --- a/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java +++ b/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java @@ -1,19 +1,18 @@ package org.implab.gradle.containers; -import org.gradle.api.GradleException; +import org.gradle.api.DefaultTask; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ProjectLayout; import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.Delete; -import org.gradle.api.tasks.TaskProvider; import org.implab.gradle.containers.cli.ImageName; import org.implab.gradle.containers.tasks.BuildImage; import org.implab.gradle.containers.tasks.PushImage; import org.implab.gradle.containers.tasks.SaveImage; -public class ContainerPlugin implements Plugin { +public abstract class ContainerPlugin implements Plugin, ProjectMixin { public static final String BUILD_GROUP = "build"; @@ -22,31 +21,19 @@ public class ContainerPlugin implements public void apply(Project project) { ProjectLayout layout = project.getLayout(); - project.getPluginManager().apply(ContainerBasePlugin.class); - - var basePlugin = project.getPlugins().findPlugin(ContainerBasePlugin.class); - if (basePlugin == null) - throw new GradleException("The container-base plugin fails to be applied"); - - var containerExtension = basePlugin.getContainerExtension(); + var basePlugin = plugin(ContainerBasePlugin.class); + var containerExtension = basePlugin.getContainerExtension(); - project.getConfigurations().create(Dependency.DEFAULT_CONFIGURATION, c -> { - c.setCanBeConsumed(true); - c.setCanBeResolved(false); - }); + configuration(Dependency.DEFAULT_CONFIGURATION, Configurations.CONSUMABLE); + configuration(ARCHIVE_CONFIGURATION, Configurations.RESOLVABLE); - project.getConfigurations().create(ARCHIVE_CONFIGURATION, c -> { - c.setCanBeConsumed(true); - c.setCanBeResolved(false); - }); - - TaskProvider processResourcesTask = project.getTasks().register("processResources", Copy.class, t -> { + var processResourcesTask = task("processResources", Copy.class, t -> { t.setGroup(BUILD_GROUP); t.from(layout.getProjectDirectory().dir("src/main")); t.into(containerExtension.getContextDirectory()); }); - TaskProvider buildImageTask = project.getTasks().register("buildImage", BuildImage.class, t -> { + var buildImageTask = task("buildImage", BuildImage.class, t -> { t.setGroup(BUILD_GROUP); t.dependsOn(processResourcesTask); @@ -56,21 +43,21 @@ public class ContainerPlugin implements t.getImageName().set(containerExtension.getImageName().map(ImageName::toString)); }); - project.getTasks().register("clean", Delete.class, t -> { + task("clean", Delete.class, t -> { t.delete(layout.getBuildDirectory()); }); - project.getTasks().register("build", t -> { + task("build", DefaultTask.class, t -> { t.setGroup(BUILD_GROUP); t.dependsOn(buildImageTask); }); - project.getTasks().register("pushImage", PushImage.class, t -> { + task("pushImage", PushImage.class, t -> { t.dependsOn(buildImageTask); t.getImageName().set(buildImageTask.flatMap(BuildImage::getImageName)); }); - project.getTasks().register("saveImage", SaveImage.class, t -> { + task("saveImage", SaveImage.class, t -> { t.dependsOn(buildImageTask); t.getExportImages().add(buildImageTask.flatMap(BuildImage::getImageName)); }); diff --git a/container/src/main/java/org/implab/gradle/containers/ProjectMixin.java b/container/src/main/java/org/implab/gradle/containers/ProjectMixin.java --- a/container/src/main/java/org/implab/gradle/containers/ProjectMixin.java +++ b/container/src/main/java/org/implab/gradle/containers/ProjectMixin.java @@ -36,7 +36,7 @@ public interface ProjectMixin { return getProject().getLayout().getProjectDirectory(); } - /** Applies and returns the specified plugin, plugin is applied only once. */ + /** Applies and returns the specified plugin, the plugin is applied only once. */ default > T plugin(Class clazz) { getProject().getPluginManager().apply(clazz); return getProject().getPlugins().findPlugin(clazz); diff --git a/container/src/main/java/org/implab/gradle/containers/cli/ComposeTraits.java b/container/src/main/java/org/implab/gradle/containers/cli/ComposeTraits.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/ComposeTraits.java @@ -0,0 +1,38 @@ +package org.implab.gradle.containers.cli; + +import java.util.List; + +public interface ComposeTraits { + public final String UP_COMMAND = "up"; + public final String EXEC_COMMAND = "exec"; + public final String STOP_COMMAND = "stop"; + public final String RM_COMMAND = "rm"; + + ProcessSpec compose(String command); + + default ProcessSpec stop() { + return compose(STOP_COMMAND); + } + + default ProcessSpec up(boolean detached) { + var spec = compose(UP_COMMAND); + if (detached) + spec.args("--detach"); + return spec; + } + + default ProcessSpec exec(String service, List options, + List command) { + return compose(EXEC_COMMAND).args(options) + .args(service) + .args(command); + } + + default ProcessSpec rm(boolean removeVolumes) { + var spec = compose(RM_COMMAND).args("--force", "--stop"); + if (removeVolumes) + spec.args("--volumes"); + + return spec; + } +} 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 @@ -3,160 +3,105 @@ package org.implab.gradle.containers.cli import java.io.File; import java.io.IOException; import java.util.List; -import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; -import org.gradle.api.logging.Logger; - public abstract class DockerTraits { - public final String BUILD_COMMAND = "build"; - public final String PUSH_COMMAND = "push"; - public final String RUN_COMMAND = "run"; - public final String SAVE_COMMAND = "save"; - public final String INSPECT_COMMAND = "inspect"; - public final String IMAGE_COMMAND = "image"; - public final String TAG_COMMAND = "tag"; - public final String COMPOSE_COMMAND = "compose"; - public final String UP_COMMAND = "up"; - public final String STOP_COMMAND = "stop"; - public final String RM_COMMAND = "rm"; - - public abstract Logger getLogger(); + private final String BUILD_COMMAND = "build"; + private final String PUSH_COMMAND = "push"; + private final String RUN_COMMAND = "run"; + private final String EXEC_COMMAND = "exec"; + private final String SAVE_COMMAND = "save"; + private final String INSPECT_COMMAND = "inspect"; + private final String IMAGE_COMMAND = "image"; + private final String TAG_COMMAND = "tag"; + private final String COMPOSE_COMMAND = "compose"; + private final String STOP_COMMAND = "stop"; + private final String START_COMMAND = "start"; + private final String CONTAINER_COMMAND = "container"; + private final String RM_COMMAND = "rm"; - public abstract Optional getWorkingDir(); - - public abstract String getCliCmd(); + protected abstract ProcessSpec builder(String... command); - protected boolean checkRetCode(ProcessSpec proc, int code) - throws InterruptedException, ExecutionException, IOException { - if (getLogger().isInfoEnabled()) { - proc.redirectStdout(RedirectTo.consumer(getLogger()::info)) - .redirectStderr(RedirectTo.consumer(getLogger()::info)); - } - - getLogger().info("Starting: {}", proc.command()); - - return proc.start().get() == code; + public ProcessSpec imageBuild(String imageName, File contextDirectory, List options) { + return builder(BUILD_COMMAND) + .args(options) + .args("-t", imageName, contextDirectory.getAbsolutePath()); } - protected void exec(ProcessSpec proc) throws InterruptedException, IOException, ExecutionException { - if (getLogger().isInfoEnabled()) - proc.redirectStdout(RedirectTo.consumer(getLogger()::info)); - - if (getLogger().isErrorEnabled()) - proc.redirectStderr(RedirectTo.consumer(getLogger()::error)); - - 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); - } + public ProcessSpec imagePush(String image, List options) { + return builder(PUSH_COMMAND) + .args(options) + .args(image); } - protected ProcessSpec builder(String... args) { - var spec = new ProcessSpec().args(getCliCmd()).args(args); - - getWorkingDir().ifPresent(spec::directory); - - return spec; - } - - public void buildImage(String imageName, File contextDirectory, List options) - 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, ExecutionException { - var spec = builder(PUSH_COMMAND) - .args(options) - .args(image); - - exec(spec); - } - - public ProcessSpec runImage(String image, List options, List command) - throws InterruptedException, IOException { + public ProcessSpec containerRun(String image, List options, List command) { return builder(RUN_COMMAND) .args(options) .args(image) .args(command); } - public void stopContainer(String containerId, List options) throws InterruptedException, IOException, ExecutionException { - exec(builder(STOP_COMMAND, containerId).args(options)); + public ProcessSpec containerExec(String containerId, List options, List command) { + return builder(EXEC_COMMAND) + .args(options) + .args(containerId) + .args(command); } - public void saveImage(Set images, File output) - throws InterruptedException, IOException, ExecutionException { - if (output.exists()) - output.delete(); - - var spec = builder(SAVE_COMMAND) - .args("-o", output.getAbsolutePath()) - .args(images); - - exec(spec); - } - - public void tagImage(String source, String target) throws InterruptedException, IOException, ExecutionException { - exec(builder(TAG_COMMAND, source, target)); + public ProcessSpec containerStop(String containerId, List options) { + return builder(STOP_COMMAND, containerId).args(options); } - public boolean imageExists(String imageId) throws InterruptedException, IOException, ExecutionException { - getLogger().info("Check image {} exists", imageId); - - return checkRetCode( - builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId), - 0); + public ProcessSpec containerStart(Set containers, List options) { + return builder(START_COMMAND).args(options).args(containers); } - public boolean imageExists(File imageIdFile) { - if (imageIdFile.exists()) { - try { - var imageId = Utils.readImageRef(imageIdFile); - return imageExists(imageId.getId()); - } catch (IOException | InterruptedException | ExecutionException e) { - getLogger().error("Failed to read imageId {}: {}", imageIdFile, e); - return false; - } - } - return false; - } + public ProcessSpec containerRm(Set containers, boolean removeVolumes, boolean force) { + var spec = builder(RM_COMMAND); - ProcessSpec compose(File primaryCompose, Set profiles, String... extra) { - var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath()); + if(removeVolumes) + spec.args("--volumes"); + if (force) + spec.args("--force"); - for (var profile : profiles) - spec.args("--profile", profile); - - spec.args(extra); + spec.args(containers); return spec; } - public void composeUp(File primaryCompose, Set profiles) throws InterruptedException, IOException, ExecutionException { - exec(compose(primaryCompose, profiles, UP_COMMAND, "--detach")); + public ProcessSpec imageSave(Set images, File output) { + if (output.exists()) + output.delete(); + + return builder(SAVE_COMMAND) + .args("-o", output.getAbsolutePath()) + .args(images); } - public void composeStop(File primaryCompose, Set profiles) throws InterruptedException, IOException, ExecutionException { - exec(compose(primaryCompose, profiles, STOP_COMMAND)); + public ProcessSpec imageTag(String source, String target) { + return builder(TAG_COMMAND, source, target); + } + + public ProcessSpec imageExists(String imageId) throws InterruptedException, IOException, ExecutionException { + return builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId); } - public void composeRm(File primaryCompose, Set profiles, boolean removeVolumes) - throws InterruptedException, IOException, ExecutionException { - var spec = compose(primaryCompose, profiles, RM_COMMAND, "--force", "--stop"); - if (removeVolumes) - spec.args("--volumes"); + public ProcessSpec containerExists(String imageId) throws InterruptedException, IOException, ExecutionException { + return builder(CONTAINER_COMMAND, INSPECT_COMMAND, "--format", "container-exists", imageId); + } - exec(spec); + public ComposeTraits compose(File primaryCompose, Set profiles) { + return new ComposeTraits() { + @Override + public ProcessSpec compose(String command) { + var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath()); + + for (var profile : profiles) + spec.args("--profile", profile); + return spec.args(command); + } + }; } } 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 --- a/container/src/main/java/org/implab/gradle/containers/cli/RedirectFrom.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/RedirectFrom.java @@ -4,7 +4,6 @@ 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 { 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 @@ -18,6 +18,9 @@ import java.util.stream.StreamSupport; import java.nio.file.Files; import java.util.List; import org.gradle.api.Action; +import org.gradle.api.GradleException; +import org.gradle.api.file.Directory; +import org.gradle.api.file.FileSystemLocation; import org.gradle.internal.impldep.org.bouncycastle.util.Iterable; import com.fasterxml.jackson.core.exc.StreamWriteException; @@ -96,19 +99,17 @@ public final class Utils { } } - public static String readAll(final File src) throws IOException { - return Files.readString(src.toPath()); + public static String readAll(final File src) { + try { + return src.isFile() ? Files.readString(src.toPath()) : null; + } catch (IOException e) { + throw new GradleException("Failed to read file " + src.toString(), e); + } } public static List readAll(final Iterable files) { return StreamSupport.stream(files.spliterator(), false) - .map(file -> { - try { - return Utils.readAll(file); - } catch (IOException e) { - throw new RuntimeException(e); - } - }) + .map(Utils::readAll) .toList(); } @@ -153,4 +154,17 @@ public final class Utils { return set.stream().map(Object::toString).collect(Collectors.toSet()); } + public static File normalizeFile(Object file) { + if (file == null) + throw new IllegalArgumentException("file"); + + if (file instanceof String) + return new File((String)file); + if (file instanceof FileSystemLocation) + return ((FileSystemLocation)file).getAsFile(); + else if (file instanceof File) + return (File)file; + throw new ClassCastException(); + } + } \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java b/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java --- a/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java +++ b/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java @@ -4,6 +4,7 @@ import java.util.concurrent.Callable; import org.gradle.api.provider.ListProperty; import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.Optional; import org.implab.gradle.containers.PropertiesMixin; import groovy.lang.Closure; @@ -11,6 +12,7 @@ import groovy.lang.Closure; public interface OptionsMixin extends PropertiesMixin { @Input + @Optional ListProperty getOptions(); default void option(String opt) { diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java b/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java --- a/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java +++ b/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java @@ -2,8 +2,8 @@ package org.implab.gradle.containers.dsl import java.util.ArrayList; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; public abstract class VolumeSpec { @@ -11,7 +11,7 @@ public abstract class VolumeSpec { public abstract Property getTarget(); - public abstract ListProperty getOptions(); + public abstract SetProperty getOptions(); public void ro() { getOptions().add("ro"); diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/BaseExecTask.java b/container/src/main/java/org/implab/gradle/containers/tasks/BaseExecTask.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/BaseExecTask.java @@ -0,0 +1,111 @@ +package org.implab.gradle.containers.tasks; + +import java.util.Optional; + +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Internal; +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; + +public abstract class BaseExecTask extends DockerCliTask implements OptionsMixin { + private final RedirectToSpec redirectStderr = new RedirectToSpec(); + + private final RedirectToSpec redirectStdout = new RedirectToSpec(); + + private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); + + private boolean detached = false; + + private boolean interactive = false; + + private boolean allocateTty = false; + + @Internal + public abstract ListProperty getCommandLine(); + + + /** + * STDIN redirection, if not specified, no input will be passed to the command + */ + @Internal + public RedirectFromSpec getStdin() { + return redirectStdin; + } + + /** + * STDOUT redirection, if not specified, redirected to logger::info + */ + @Internal + public RedirectToSpec getStdout() { + return redirectStdout; + } + + /** + * STDERR redirection, if not specified, redirected to logger::error + */ + @Internal + public RedirectToSpec getStderr() { + return redirectStderr; + } + + /** + * Specified whether the container should run in background. If the container + * has explicit IO redirects it will run in the foreground. + */ + @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 getAllocateTty() { + return allocateTty; + } + + public void setAllocateTty(boolean value) { + allocateTty = value; + } + + /** + * Specified the interactive flag for the container, if the task specified + * STDIN redirection this flag is enabled automatically. + */ + @Internal + public boolean isInteractive() { + // enable interactive mode when processing standard input + return redirectStdin.isRedirected() || interactive; + } + + /** Appends specified parameters to the command line */ + void commandLine(String... args) { + getCommandLine().addAll(args); + } + + + @Override + protected Optional stdoutRedirection() { + return redirectStdout.getRedirection().or(super::stdoutRedirection); + } + + @Override + protected Optional stderrRedirection() { + return redirectStderr.getRedirection().or(super::stderrRedirection); + } + + @Override + protected Optional stdinRedirection() { + return redirectStdin.getRedirection(); + } + +} 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 @@ -21,6 +21,7 @@ import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.SkipWhenEmpty; import org.gradle.api.tasks.TaskAction; import java.io.File; +import java.io.IOException; import org.implab.gradle.containers.cli.ImageRef; import org.implab.gradle.containers.cli.Utils; @@ -55,8 +56,7 @@ public abstract class BuildImage extends public BuildImage() { getOutputs().upToDateWhen(task -> getImageIdFile() - .map(RegularFile::getAsFile) - .map(docker()::imageExists) + .map(this::imageExists) .getOrElse(false)); } @@ -77,12 +77,26 @@ public abstract class BuildImage extends return new MapPropertyEntry(getBuildArgs(), key, getProviders()); } + private boolean imageExists(RegularFile file) { + return readRefId(file.getAsFile()).map(this::imageExists).orElse(false); + } + + private java.util.Optional readRefId(File idFile) { + try { + return idFile.exists() ? java.util.Optional.of(Utils.readImageRef(idFile).getId()) + : java.util.Optional.empty(); + } catch (IOException e) { + getLogger().error("Failed to read imageId {}: {}", idFile, e); + return java.util.Optional.empty(); + } + } + @TaskAction public void run() throws Exception { List args = new ArrayList<>(); // create a temp file to store image id - var iidFile = new File(this.getTemporaryDir(), "iid"); + var iidFile = new File(this.getTemporaryDir(), "imageid"); // specify where to write image id args.addAll(List.of("--iidfile", iidFile.getAbsolutePath())); @@ -104,11 +118,13 @@ public abstract class BuildImage extends var imageTag = getImageName().map(Object::toString).get(); // build image - docker().buildImage( + var spec = docker().imageBuild( imageTag, getContextDirectory().map(Directory::getAsFile).get(), args); + exec(spec); + // read the built image id and store image ref metadata var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile)); Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef); 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 --- a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeExec.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeExec.java @@ -1,10 +1,31 @@ package org.implab.gradle.containers.tasks; +import java.io.IOException; +import java.util.ArrayList; +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 ComposeExec { - +public abstract class ComposeExec extends BaseExecTask implements ComposeTaskMixin { + @Internal public abstract Property getServiceName(); + + public ComposeExec() { + applyComposeConvention(); + } + + @TaskAction + public void run() throws InterruptedException, ExecutionException, IOException { + var params = new ArrayList<>(getOptions().get()); + + if (isDetached()) + params.add("--detach"); + if (!getAllocateTty()) + params.add("--no-TTY"); + + exec(docker().compose(this).exec(getServiceName().get(), params, getCommandLine().get())); + } } 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 @@ -7,17 +7,18 @@ import org.gradle.api.provider.Property; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; -public abstract class ComposeRm extends ComposeTask { - +public abstract class ComposeRm extends DockerCliTask implements ComposeTaskMixin { + @Internal public abstract Property getRemoveVolumes(); public ComposeRm() { + applyComposeConvention(); getRemoveVolumes().convention(true); } @TaskAction public void run() throws InterruptedException, IOException, ExecutionException { - docker().composeRm(getComposeFile().get().getAsFile(), getProfiles().get(), getRemoveVolumes().get()); + exec(docker().compose(this).rm(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 @@ -5,10 +5,14 @@ import java.util.concurrent.ExecutionExc import org.gradle.api.tasks.TaskAction; -public abstract class ComposeStop extends ComposeTask { +public abstract class ComposeStop extends DockerCliTask implements ComposeTaskMixin { + + public ComposeStop() { + applyComposeConvention(); + } @TaskAction public void run() throws InterruptedException, IOException, ExecutionException { - docker().composeStop(getComposeFile().getAsFile().get(), getProfiles().get()); + exec(docker().compose(this).stop()); } } \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeTask.java b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeTaskMixin.java rename from container/src/main/java/org/implab/gradle/containers/tasks/ComposeTask.java rename to container/src/main/java/org/implab/gradle/containers/tasks/ComposeTaskMixin.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/ComposeTask.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ComposeTaskMixin.java @@ -1,33 +1,37 @@ package org.implab.gradle.containers.tasks; +import java.io.File; +import java.util.Optional; + +import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.SetProperty; import org.gradle.api.tasks.Internal; +import org.implab.gradle.containers.ComposeExtension; /** * Base task for compose subtasks like 'uo', 'rm', 'stop', etc. */ -public abstract class ComposeTask extends DockerCliTask { +public interface ComposeTaskMixin extends TaskServices { /** The list of profiles to use with the compose commands */ @Internal - public abstract SetProperty getProfiles(); + SetProperty getProfiles(); /** * The primary compose files. This task is executed regardless whether these * files was changed. */ @Internal - public abstract RegularFileProperty getComposeFile(); + RegularFileProperty getComposeFile(); - protected ComposeTask() { - // the task can only be evaluated if the compose file is present - setOnlyIf(self -> { - var composeFile = getComposeFile().get().getAsFile(); - var exists = composeFile.exists(); - getLogger().info("file: {} {}", composeFile.toString(), exists ? "exists" : "doesn't exist"); - return exists; - }); + default void applyComposeConvention() { + onlyIf(task -> getComposeFile().map(RegularFile::getAsFile).map(File::exists).getOrElse(false)); + Optional.ofNullable(getProject().getExtensions().findByType(ComposeExtension.class)) + .ifPresent(ext -> { + getProfiles().set(ext.getProfiles()); + getComposeFile().set(ext.getComposeFile()); + }); + } - } 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 @@ -3,12 +3,29 @@ package org.implab.gradle.containers.tas import java.io.IOException; import java.util.concurrent.ExecutionException; +import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.TaskAction; -public abstract class ComposeUp extends ComposeTask { +public abstract class ComposeUp extends DockerCliTask implements ComposeTaskMixin { + + private boolean detached = true; + + @Internal + public boolean isDetached() { + return detached; + } + + public void setDetached(boolean value) { + detached = value; + } + + public ComposeUp() { + applyComposeConvention(); + } @TaskAction public void run() throws InterruptedException, IOException, ExecutionException { - docker().composeUp(getComposeFile().getAsFile().get(), getProfiles().get()); + + exec(docker().compose(this).up(isDetached())); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ContainerTaskMixin.java b/container/src/main/java/org/implab/gradle/containers/tasks/ContainerTaskMixin.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ContainerTaskMixin.java @@ -0,0 +1,22 @@ +package org.implab.gradle.containers.tasks; + +import java.io.File; + +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.Internal; +import org.implab.gradle.containers.PropertiesMixin; +import org.implab.gradle.containers.cli.Utils; + +public interface ContainerTaskMixin extends TaskServices, PropertiesMixin { + @Internal + Property getContainerId(); + + default void fromIdFile(File file) { + getContainerId().set(provider(() -> Utils.readAll(file))); + } + + default void fromIdFile(Provider file) { + getContainerId().set(file.map(Utils::normalizeFile).map(Utils::readAll)); + } +} 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 @@ -1,19 +1,24 @@ package org.implab.gradle.containers.tasks; import java.io.File; +import java.io.IOException; import java.util.Optional; +import java.util.concurrent.ExecutionException; import org.gradle.api.DefaultTask; -import org.gradle.api.logging.Logger; +import org.gradle.api.GradleException; +import org.gradle.api.file.RegularFile; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.implab.gradle.containers.ContainerExtension; import org.implab.gradle.containers.PropertiesMixin; +import org.implab.gradle.containers.cli.ComposeTraits; 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; +import org.implab.gradle.containers.cli.Utils; public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin { @@ -45,44 +50,100 @@ public abstract class DockerCliTask exte return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty(); } - protected Optional getStdoutRedirection() { + protected Optional stdoutRedirection() { return loggerInfoRedirect(); } - protected Optional getStderrRedirection() { + protected Optional stderrRedirection() { return loggerErrorRedirect(); } - protected Optional getStdinRedirection() { + protected Optional stdinRedirection() { return Optional.empty(); } - protected DockerTraits docker() { + protected TaskDockerTraits docker() { return new TaskDockerTraits(); } - protected void exec(ProcessSpec spec) { + protected boolean imageExists(String imageId) { + try { + return checkRetCode(docker().imageExists(imageId), 0); + } catch (InterruptedException | ExecutionException | IOException e) { + // wrap to unchecked exception + throw new GradleException("Failed to execute imageExists", e); + } + } + + protected boolean containerExists(String containerId) { + try { + return checkRetCode(docker().containerExists(containerId), 0); + } catch (InterruptedException | ExecutionException | IOException e) { + // wrap to unchecked exception + throw new GradleException("Failed to execute imageExists", e); + } + } + + protected void exec(ProcessSpec spec) throws InterruptedException, ExecutionException, IOException { + + stdoutRedirection().ifPresent(spec::redirectStdout); + stderrRedirection().ifPresent(spec::redirectStderr); + stdinRedirection().ifPresent(spec::redirectStdin); + + getLogger().info("Staring: {}", spec.command()); + + // runs the command and checks the error code + spec.exec(); + } - getLogger().info("Starting: {}", spec.command()); + protected boolean checkRetCode(ProcessSpec proc, int code) + throws InterruptedException, ExecutionException, IOException { + if (getLogger().isInfoEnabled()) { + proc.redirectStdout(RedirectTo.consumer(getLogger()::info)) + .redirectStderr(RedirectTo.consumer(getLogger()::info)); + } + + getLogger().info("Starting: {}", proc.command()); + + return proc.start().get() == code; + } + + /** + * Helper function to read + * @param idFile + * @return + */ + protected String readId(File idFile) { + if (idFile.exists()) { + try { + return Utils.readImageRef(idFile).getId(); + } catch (IOException e) { + getLogger().error("Failed to read imageId {}: {}", idFile, e); + return null; + } + } else { + return null; + } + } + + protected ProcessSpec commandBuilder(String... command) { + var spec = new ProcessSpec().args(getCliCmd().get()).args(command); + + if (getWorkingDirectory().isPresent()) + spec.directory(getWorkingDirectory().get()); + + return spec; } class TaskDockerTraits extends DockerTraits { @Override - public Logger getLogger() { - return DockerCliTask.this.getLogger(); + protected ProcessSpec builder(String... command) { + return commandBuilder(command); } - @Override - public Optional getWorkingDir() { - return getWorkingDirectory() - .map(Optional::of) - .getOrElse(Optional.empty()); - } - - @Override - public String getCliCmd() { - return DockerCliTask.this.getCliCmd().get(); + public ComposeTraits compose(ComposeTaskMixin props) { + return compose(props.getComposeFile().map(RegularFile::getAsFile).get(), props.getProfiles().get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/ExecContainer.java b/container/src/main/java/org/implab/gradle/containers/tasks/ExecContainer.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/ExecContainer.java @@ -0,0 +1,32 @@ +package org.implab.gradle.containers.tasks; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; + +import org.gradle.api.tasks.TaskAction; + +public abstract class ExecContainer extends BaseExecTask implements ContainerTaskMixin { + + public ExecContainer() { + onlyIfReason("No container specified", self -> getContainerId().isPresent()); + } + + @TaskAction + public void run() throws InterruptedException, IOException, ExecutionException { + var params = new ArrayList<>(getOptions().get()); + + if (isInteractive()) + params.add("--interactive"); + if (isDetached()) + params.add("--detach"); + if (getAllocateTty()) + params.add("--tty"); + + exec(docker().containerExec( + getContainerId().get(), + params, + getCommandLine().get())); + } + +} 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 @@ -3,7 +3,6 @@ package org.implab.gradle.containers.tas import java.io.IOException; import java.util.concurrent.ExecutionException; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; @@ -19,15 +18,11 @@ public abstract class PushImage extends @Optional public abstract Property getTransport(); - @Input - @Optional - public abstract ListProperty getOptions(); - @TaskAction public void run() throws InterruptedException, IOException, ExecutionException { - docker().pushImage( + exec(docker().imagePush( getImageName().map(Object::toString).get(), - getOptions().get()); + getOptions().get())); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/RmContainer.java b/container/src/main/java/org/implab/gradle/containers/tasks/RmContainer.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/RmContainer.java @@ -0,0 +1,50 @@ +package org.implab.gradle.containers.tasks; + +import java.io.File; +import java.io.IOException; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.SetProperty; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.Utils; + +public abstract class RmContainer extends DockerCliTask implements TaskServices { + + @Internal + public abstract Property getRemoveVolumes(); + + @Internal + public abstract Property getForce(); + + @Internal + public abstract SetProperty getContainerIds(); + + public RmContainer() { + getRemoveVolumes().convention(true); + getForce().convention(false); + onlyIfReason("No containers specified", self -> getContainerIds().get().size() > 0); + } + + public void fromIdFile(File file) { + getContainerIds().add(provider(() -> Utils.readAll(file))); + } + + public void fromIdFile(Provider file) { + getContainerIds().add(file.map(Utils::normalizeFile).map(Utils::readAll)); + } + + @TaskAction + public void run() throws InterruptedException, ExecutionException, IOException { + var alive = getContainerIds().get().stream().filter(this::containerExists).collect(Collectors.toSet()); + + if (alive.size() > 0) + exec(docker().containerRm(alive, getRemoveVolumes().get(), getForce().get())); + else + getLogger().info("No containers left, nothing to do"); + } + +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java b/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/RunContainer.java @@ -4,57 +4,32 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; -import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import org.gradle.api.Action; +import org.gradle.api.GradleException; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; 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 RunContainer extends DockerCliTask implements OptionsMixin { - - private final RedirectToSpec redirectStderr = new RedirectToSpec(); - - private final RedirectToSpec redirectStdout = new RedirectToSpec(); - - private final RedirectFromSpec redirectStdin = new RedirectFromSpec(); +public abstract class RunContainer extends BaseExecTask implements ContainerTaskMixin { private boolean transientContainer = true; - private boolean detached = false; - - private boolean interactive = false; + private boolean startIfExists = true; @Internal public abstract Property getImageName(); @Internal - public abstract ListProperty getCommandLine(); - - @Internal - public RedirectFromSpec getStdin() { - return redirectStdin; - } + public abstract Property getContainerName(); - @Internal - public RedirectToSpec getStdout() { - return redirectStdout; - } - - @Internal - public RedirectToSpec getStderr() { - return redirectStderr; - } - + /** + * Transient containers will be removed after completion. Default is `true` + */ @Internal public boolean isTransientContainer() { return transientContainer; @@ -65,24 +40,22 @@ public abstract class RunContainer exten } @Internal - public boolean isDetached() { - // if IO was redirected the container should run in foreground - if (redirectStdin.isRedirected() || redirectStdout.isRedirected() || redirectStderr.isRedirected()) - return false; + public boolean getStartIfExists() { + return startIfExists; + } - return detached; + public void setStartIdExists(boolean value) { + startIfExists = value; } - public void setDetached(boolean value) { - detached = value; - } - - @Internal - public boolean isInteractive() { - // enable interactive mode when processing standard input - return redirectStdin.isRedirected() || interactive; - } - + /** + * Specified the file where the stared container id will be written. + * Default value is a temporary file. + * + *

+ * This property can be use in tasks where the container id is required + * to perform operation, for example to StopContainer tasks. + */ @Internal public abstract RegularFileProperty getContainerIdFile(); @@ -90,6 +63,11 @@ public abstract class RunContainer exten getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid")); } + /** + * Adds volume specification + * + * @param configure + */ public void volume(Action configure) { getOptions().add("-v"); getOptions().add(provider(() -> { @@ -99,61 +77,49 @@ public abstract class RunContainer exten })); } - 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, 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(); + if (getContainerId().map(this::containerExists).getOrElse(false)) { + var cid = getContainerId().get(); + if (getStartIfExists()) { + getLogger().info("Container {} already created, starting.", cid); - params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString())); - } + if (isInteractive()) + params.add("--interactive"); + if (!isDetached()) + params.add("--attach"); + + exec(docker().containerStart(Set.of(cid), params)); + } else { + throw new GradleException("Container " + cid + " already exists"); + } + } else { - 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); + if (isInteractive()) + params.add("--interactive"); + if (isTransientContainer()) + params.add("--rm"); + if (isDetached()) + params.add("--detach"); + if (getAllocateTty()) + params.add("--tty"); + if (getContainerName().isPresent()) + params.addAll(List.of("--name", getContainerName().get())); - redirectStderr.getRedirection() - .or(this::loggerErrorRedirect) - .ifPresent(spec::redirectStderr); + if (getContainerIdFile().isPresent()) { + // delete previous container id otherwise the container will fail to start + getContainerIdFile().getAsFile().get().delete(); - redirectStdin.getRedirection().ifPresent(spec::redirectStdin); + params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString())); + } - getLogger().info("Staring: {}", spec.command()); - - // runs the command and checks the error code - spec.exec(); + exec(docker().containerRun( + getImageName().map(Object::toString).get(), + params, + getCommandLine().get())); + } } } 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 @@ -105,8 +105,8 @@ public abstract class SaveImage extends @TaskAction public void run() throws InterruptedException, IOException, ExecutionException { - docker().saveImage( + exec(docker().imageSave( getExportImages().map(Utils::mapToString).get(), - getArchiveFile().map(RegularFile::getAsFile).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 --- a/container/src/main/java/org/implab/gradle/containers/tasks/StopContainer.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/StopContainer.java @@ -5,22 +5,11 @@ 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(); +public abstract class StopContainer extends DockerCliTask implements ContainerTaskMixin { @Internal public abstract Property getStopSignal(); @@ -28,27 +17,12 @@ public abstract class StopContainer exte @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); - } - }); + public StopContainer() { + onlyIfReason("Container doesn't exists", self -> getContainerId().map(this::containerExists).getOrElse(false)); } @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()) @@ -57,10 +31,7 @@ public abstract class StopContainer exte 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(); + exec(docker().containerStop(getContainerId().get(), options)); } } 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 @@ -28,7 +28,7 @@ public abstract class TagImage extends D } public TagImage() { - this.setOnlyIf("No tags were specified", self -> getImageTags().size() > 0); + this.setOnlyIf(self -> getImageTags().size() > 0); } @TaskAction @@ -36,12 +36,9 @@ public abstract class TagImage extends D var tags = getImageTags(); var src = getSrcImage().map(Object::toString).get(); - if (tags.size() == 0) - getLogger().info("No tags were specified"); - for (var tag : tags) { getLogger().info("Tag: {}", tag); - docker().tagImage(src, tag); + exec(docker().imageTag(src, tag)); } } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/TaskServices.java b/container/src/main/java/org/implab/gradle/containers/tasks/TaskServices.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/tasks/TaskServices.java @@ -0,0 +1,29 @@ +package org.implab.gradle.containers.tasks; + +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.logging.Logger; +import org.gradle.api.specs.Spec; +import org.gradle.api.tasks.Internal; + +/** Task methods available by default, this interface is used by mixins to + * interact with their task. + */ +public interface TaskServices { + @Internal + Project getProject(); + + void onlyIf(Spec spec); + + @Internal + Logger getLogger(); + + default void onlyIfReason(String skipReason, Spec spec) { + onlyIf(self -> { + var satisfied = spec.isSatisfiedBy(self); + if (!satisfied) + getLogger().info("SKIP: {}", skipReason); + return satisfied; + }); + } +}