##// END OF EJS Templates
Implemented RunContainer, ExecContainer, StopContainer, RmContainer, ComposeExec tasks
cin -
r19:5e6d0e24a5d2 v1.3.0 default
parent child
Show More
@@ -0,0 +1,38
1 package org.implab.gradle.containers.cli;
2
3 import java.util.List;
4
5 public interface ComposeTraits {
6 public final String UP_COMMAND = "up";
7 public final String EXEC_COMMAND = "exec";
8 public final String STOP_COMMAND = "stop";
9 public final String RM_COMMAND = "rm";
10
11 ProcessSpec compose(String command);
12
13 default ProcessSpec stop() {
14 return compose(STOP_COMMAND);
15 }
16
17 default ProcessSpec up(boolean detached) {
18 var spec = compose(UP_COMMAND);
19 if (detached)
20 spec.args("--detach");
21 return spec;
22 }
23
24 default ProcessSpec exec(String service, List<String> options,
25 List<String> command) {
26 return compose(EXEC_COMMAND).args(options)
27 .args(service)
28 .args(command);
29 }
30
31 default ProcessSpec rm(boolean removeVolumes) {
32 var spec = compose(RM_COMMAND).args("--force", "--stop");
33 if (removeVolumes)
34 spec.args("--volumes");
35
36 return spec;
37 }
38 }
@@ -0,0 +1,111
1 package org.implab.gradle.containers.tasks;
2
3 import java.util.Optional;
4
5 import org.gradle.api.provider.ListProperty;
6 import org.gradle.api.tasks.Internal;
7 import org.implab.gradle.containers.cli.RedirectFrom;
8 import org.implab.gradle.containers.cli.RedirectTo;
9 import org.implab.gradle.containers.dsl.OptionsMixin;
10 import org.implab.gradle.containers.dsl.RedirectFromSpec;
11 import org.implab.gradle.containers.dsl.RedirectToSpec;
12
13 public abstract class BaseExecTask extends DockerCliTask implements OptionsMixin {
14 private final RedirectToSpec redirectStderr = new RedirectToSpec();
15
16 private final RedirectToSpec redirectStdout = new RedirectToSpec();
17
18 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
19
20 private boolean detached = false;
21
22 private boolean interactive = false;
23
24 private boolean allocateTty = false;
25
26 @Internal
27 public abstract ListProperty<String> getCommandLine();
28
29
30 /**
31 * STDIN redirection, if not specified, no input will be passed to the command
32 */
33 @Internal
34 public RedirectFromSpec getStdin() {
35 return redirectStdin;
36 }
37
38 /**
39 * STDOUT redirection, if not specified, redirected to logger::info
40 */
41 @Internal
42 public RedirectToSpec getStdout() {
43 return redirectStdout;
44 }
45
46 /**
47 * STDERR redirection, if not specified, redirected to logger::error
48 */
49 @Internal
50 public RedirectToSpec getStderr() {
51 return redirectStderr;
52 }
53
54 /**
55 * Specified whether the container should run in background. If the container
56 * has explicit IO redirects it will run in the foreground.
57 */
58 @Internal
59 public boolean isDetached() {
60 // if IO was redirected the container should run in foreground
61 if (redirectStdin.isRedirected() || redirectStdout.isRedirected() || redirectStderr.isRedirected())
62 return false;
63
64 return detached;
65 }
66
67 public void setDetached(boolean value) {
68 detached = value;
69 }
70
71 @Internal
72 public boolean getAllocateTty() {
73 return allocateTty;
74 }
75
76 public void setAllocateTty(boolean value) {
77 allocateTty = value;
78 }
79
80 /**
81 * Specified the interactive flag for the container, if the task specified
82 * STDIN redirection this flag is enabled automatically.
83 */
84 @Internal
85 public boolean isInteractive() {
86 // enable interactive mode when processing standard input
87 return redirectStdin.isRedirected() || interactive;
88 }
89
90 /** Appends specified parameters to the command line */
91 void commandLine(String... args) {
92 getCommandLine().addAll(args);
93 }
94
95
96 @Override
97 protected Optional<RedirectTo> stdoutRedirection() {
98 return redirectStdout.getRedirection().or(super::stdoutRedirection);
99 }
100
101 @Override
102 protected Optional<RedirectTo> stderrRedirection() {
103 return redirectStderr.getRedirection().or(super::stderrRedirection);
104 }
105
106 @Override
107 protected Optional<RedirectFrom> stdinRedirection() {
108 return redirectStdin.getRedirection();
109 }
110
111 }
@@ -0,0 +1,22
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.File;
4
5 import org.gradle.api.provider.Property;
6 import org.gradle.api.provider.Provider;
7 import org.gradle.api.tasks.Internal;
8 import org.implab.gradle.containers.PropertiesMixin;
9 import org.implab.gradle.containers.cli.Utils;
10
11 public interface ContainerTaskMixin extends TaskServices, PropertiesMixin {
12 @Internal
13 Property<String> getContainerId();
14
15 default void fromIdFile(File file) {
16 getContainerId().set(provider(() -> Utils.readAll(file)));
17 }
18
19 default void fromIdFile(Provider<Object> file) {
20 getContainerId().set(file.map(Utils::normalizeFile).map(Utils::readAll));
21 }
22 }
@@ -0,0 +1,32
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.concurrent.ExecutionException;
6
7 import org.gradle.api.tasks.TaskAction;
8
9 public abstract class ExecContainer extends BaseExecTask implements ContainerTaskMixin {
10
11 public ExecContainer() {
12 onlyIfReason("No container specified", self -> getContainerId().isPresent());
13 }
14
15 @TaskAction
16 public void run() throws InterruptedException, IOException, ExecutionException {
17 var params = new ArrayList<>(getOptions().get());
18
19 if (isInteractive())
20 params.add("--interactive");
21 if (isDetached())
22 params.add("--detach");
23 if (getAllocateTty())
24 params.add("--tty");
25
26 exec(docker().containerExec(
27 getContainerId().get(),
28 params,
29 getCommandLine().get()));
30 }
31
32 }
@@ -0,0 +1,50
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.concurrent.ExecutionException;
6 import java.util.stream.Collectors;
7
8 import org.gradle.api.provider.Property;
9 import org.gradle.api.provider.Provider;
10 import org.gradle.api.provider.SetProperty;
11 import org.gradle.api.tasks.Internal;
12 import org.gradle.api.tasks.TaskAction;
13 import org.implab.gradle.containers.cli.Utils;
14
15 public abstract class RmContainer extends DockerCliTask implements TaskServices {
16
17 @Internal
18 public abstract Property<Boolean> getRemoveVolumes();
19
20 @Internal
21 public abstract Property<Boolean> getForce();
22
23 @Internal
24 public abstract SetProperty<String> getContainerIds();
25
26 public RmContainer() {
27 getRemoveVolumes().convention(true);
28 getForce().convention(false);
29 onlyIfReason("No containers specified", self -> getContainerIds().get().size() > 0);
30 }
31
32 public void fromIdFile(File file) {
33 getContainerIds().add(provider(() -> Utils.readAll(file)));
34 }
35
36 public void fromIdFile(Provider<Object> file) {
37 getContainerIds().add(file.map(Utils::normalizeFile).map(Utils::readAll));
38 }
39
40 @TaskAction
41 public void run() throws InterruptedException, ExecutionException, IOException {
42 var alive = getContainerIds().get().stream().filter(this::containerExists).collect(Collectors.toSet());
43
44 if (alive.size() > 0)
45 exec(docker().containerRm(alive, getRemoveVolumes().get(), getForce().get()));
46 else
47 getLogger().info("No containers left, nothing to do");
48 }
49
50 }
@@ -0,0 +1,29
1 package org.implab.gradle.containers.tasks;
2
3 import org.gradle.api.Project;
4 import org.gradle.api.Task;
5 import org.gradle.api.logging.Logger;
6 import org.gradle.api.specs.Spec;
7 import org.gradle.api.tasks.Internal;
8
9 /** Task methods available by default, this interface is used by mixins to
10 * interact with their task.
11 */
12 public interface TaskServices {
13 @Internal
14 Project getProject();
15
16 void onlyIf(Spec<? super Task> spec);
17
18 @Internal
19 Logger getLogger();
20
21 default void onlyIfReason(String skipReason, Spec<? super Task> spec) {
22 onlyIf(self -> {
23 var satisfied = spec.isSatisfiedBy(self);
24 if (!satisfied)
25 getLogger().info("SKIP: {}", skipReason);
26 return satisfied;
27 });
28 }
29 }
@@ -1,6 +1,12
1 package org.implab.gradle.containers;
1 package org.implab.gradle.containers;
2
2
3 import javax.inject.Inject;
4
5 import org.gradle.api.file.DirectoryProperty;
6 import org.gradle.api.file.ProjectLayout;
7 import org.gradle.api.file.RegularFile;
3 import org.gradle.api.provider.Property;
8 import org.gradle.api.provider.Property;
9 import org.gradle.api.provider.Provider;
4 import org.gradle.api.provider.SetProperty;
10 import org.gradle.api.provider.SetProperty;
5
11
6 public abstract class ComposeExtension {
12 public abstract class ComposeExtension {
@@ -8,9 +14,17 public abstract class ComposeExtension {
8
14
9 public abstract SetProperty<String> getProfiles();
15 public abstract SetProperty<String> getProfiles();
10
16
17 public abstract DirectoryProperty getContextDirectory();
18
11 public abstract Property<String> getComposeFileName();
19 public abstract Property<String> getComposeFileName();
12
20
13 public ComposeExtension() {
21 public Provider<RegularFile> getComposeFile() {
22 return getContextDirectory().file(getComposeFileName());
23 }
24
25 @Inject
26 public ComposeExtension(ProjectLayout layout) {
27 getContextDirectory().convention(layout.getBuildDirectory().dir("context"));
14 getComposeFileName().convention(COMPOSE_FILE);
28 getComposeFileName().convention(COMPOSE_FILE);
15 }
29 }
16
30
@@ -54,30 +54,25 public abstract class ComposePlugin impl
54 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
54 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
55
55
56 // basic configuration, register extension
56 // basic configuration, register extension
57 var basePlugin = plugin(ContainerBasePlugin.class);
57 plugin(ContainerBasePlugin.class);
58 var containerExtension = basePlugin.getContainerExtension();
59
58
60 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
59 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
61
60
62 var composeFile = containerExtension.getContextDirectory()
63 .file(composeExtension.getComposeFileName());
64 var composeProfiles = composeExtension.getProfiles();
65
66 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
61 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
67 t.delete(containerExtension.getContextDirectory());
62 t.delete(composeExtension.getContextDirectory());
68 });
63 });
69
64
70 // copy task from src/main
65 // copy task from src/main
71 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
66 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
72 t.mustRunAfter(cleanTask);
67 t.mustRunAfter(cleanTask);
73 t.from(projectDirectory().dir("src/main"));
68 t.from(projectDirectory().dir("src/main"));
74 t.into(containerExtension.getContextDirectory());
69 t.into(composeExtension.getContextDirectory());
75 });
70 });
76
71
77 // write .env
72 // write .env
78 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
73 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
79 t.dependsOn(processResources, containerImages);
74 t.dependsOn(processResources, containerImages);
80 t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME));
75 t.getEnvFile().set(composeExtension.getContextDirectory().file(ENV_FILE_NAME));
81
76
82 var group = project.getGroup();
77 var group = project.getGroup();
83 if (group != null && group.toString().length() > 0) {
78 if (group != null && group.toString().length() > 0) {
@@ -97,16 +92,12 public abstract class ComposePlugin impl
97 // stop must run after build
92 // stop must run after build
98 t.mustRunAfter(buildTask);
93 t.mustRunAfter(buildTask);
99
94
100 t.getProfiles().addAll(composeProfiles);
101 t.getComposeFile().set(composeFile);
102 });
95 });
103
96
104 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
97 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
105 // rm must run after build and stop
98 // rm must run after build and stop
106 t.mustRunAfter(buildTask, stopTask);
99 t.mustRunAfter(buildTask, stopTask);
107
100
108 t.getProfiles().addAll(composeProfiles);
109 t.getComposeFile().set(composeFile);
110 });
101 });
111
102
112 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
103 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
@@ -114,8 +105,6 public abstract class ComposePlugin impl
114 // up must run after stop and rm
105 // up must run after stop and rm
115 t.mustRunAfter(stopTask, rmTask);
106 t.mustRunAfter(stopTask, rmTask);
116
107
117 t.getProfiles().addAll(composeProfiles);
118 t.getComposeFile().set(composeFile);
119 });
108 });
120 }
109 }
121
110
@@ -4,13 +4,19 import org.gradle.api.Plugin;
4 import org.gradle.api.Project;
4 import org.gradle.api.Project;
5 import org.gradle.api.plugins.ExtraPropertiesExtension;
5 import org.gradle.api.plugins.ExtraPropertiesExtension;
6 import org.implab.gradle.containers.tasks.BuildImage;
6 import org.implab.gradle.containers.tasks.BuildImage;
7 import org.implab.gradle.containers.tasks.ComposeExec;
8 import org.implab.gradle.containers.tasks.ComposeRm;
9 import org.implab.gradle.containers.tasks.ComposeStop;
10 import org.implab.gradle.containers.tasks.ComposeUp;
11 import org.implab.gradle.containers.tasks.ExecContainer;
7 import org.implab.gradle.containers.tasks.PushImage;
12 import org.implab.gradle.containers.tasks.PushImage;
13 import org.implab.gradle.containers.tasks.RmContainer;
8 import org.implab.gradle.containers.tasks.RunContainer;
14 import org.implab.gradle.containers.tasks.RunContainer;
9 import org.implab.gradle.containers.tasks.SaveImage;
15 import org.implab.gradle.containers.tasks.SaveImage;
10 import org.implab.gradle.containers.tasks.StopContainer;
16 import org.implab.gradle.containers.tasks.StopContainer;
11 import org.implab.gradle.containers.tasks.TagImage;
17 import org.implab.gradle.containers.tasks.TagImage;
12
18
13 public class ContainerBasePlugin implements Plugin<Project> {
19 public abstract class ContainerBasePlugin implements Plugin<Project>, ProjectMixin {
14 public static final String CONTAINER_EXTENSION_NAME = "container";
20 public static final String CONTAINER_EXTENSION_NAME = "container";
15
21
16 private ContainerExtension containerExtension;
22 private ContainerExtension containerExtension;
@@ -30,26 +36,13 public class ContainerBasePlugin impleme
30 @Override
36 @Override
31 public void apply(Project project) {
37 public void apply(Project project) {
32
38
33 containerExtension = project.getObjects().newInstance(ContainerExtension.class);
39 containerExtension = extension(CONTAINER_EXTENSION_NAME, ContainerExtension.class);
34
35 // TODO: move properties initialization into the constructor
36 containerExtension.getImageAuthority()
37 .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority")));
38
39 containerExtension.getImageGroup()
40 .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup")));
41
42 containerExtension.getCliCmd()
43 .convention(project
44 .provider(() -> (String) project.getProperties().get("containerCli"))
45 .orElse("docker"));
46
47 project.getExtensions().add(CONTAINER_EXTENSION_NAME, containerExtension);
48
40
49 exportClasses(
41 exportClasses(
50 project,
42 project,
51 BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunContainer.class,
43 BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunContainer.class,
52 StopContainer.class);
44 StopContainer.class, ExecContainer.class, RmContainer.class, ComposeUp.class, ComposeExec.class,
45 ComposeRm.class, ComposeStop.class);
53
46
54 }
47 }
55
48
@@ -61,6 +61,16 public abstract class ContainerExtension
61 getImageTag().set(provider(() -> Optional.ofNullable(project.getVersion())
61 getImageTag().set(provider(() -> Optional.ofNullable(project.getVersion())
62 .map(Object::toString)
62 .map(Object::toString)
63 .orElse("latest")));
63 .orElse("latest")));
64
65 getImageAuthority()
66 .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority")));
67
68 getImageGroup()
69 .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup")));
70
71 getCliCmd().convention(project
72 .provider(() -> (String) project.getProperties().get("containerCli"))
73 .orElse("docker"));
64 }
74 }
65
75
66 ImageName createImageName() {
76 ImageName createImageName() {
@@ -1,19 +1,18
1 package org.implab.gradle.containers;
1 package org.implab.gradle.containers;
2
2
3 import org.gradle.api.GradleException;
3 import org.gradle.api.DefaultTask;
4 import org.gradle.api.Plugin;
4 import org.gradle.api.Plugin;
5 import org.gradle.api.Project;
5 import org.gradle.api.Project;
6 import org.gradle.api.artifacts.Dependency;
6 import org.gradle.api.artifacts.Dependency;
7 import org.gradle.api.file.ProjectLayout;
7 import org.gradle.api.file.ProjectLayout;
8 import org.gradle.api.tasks.Copy;
8 import org.gradle.api.tasks.Copy;
9 import org.gradle.api.tasks.Delete;
9 import org.gradle.api.tasks.Delete;
10 import org.gradle.api.tasks.TaskProvider;
11 import org.implab.gradle.containers.cli.ImageName;
10 import org.implab.gradle.containers.cli.ImageName;
12 import org.implab.gradle.containers.tasks.BuildImage;
11 import org.implab.gradle.containers.tasks.BuildImage;
13 import org.implab.gradle.containers.tasks.PushImage;
12 import org.implab.gradle.containers.tasks.PushImage;
14 import org.implab.gradle.containers.tasks.SaveImage;
13 import org.implab.gradle.containers.tasks.SaveImage;
15
14
16 public class ContainerPlugin implements Plugin<Project> {
15 public abstract class ContainerPlugin implements Plugin<Project>, ProjectMixin {
17
16
18 public static final String BUILD_GROUP = "build";
17 public static final String BUILD_GROUP = "build";
19
18
@@ -22,31 +21,19 public class ContainerPlugin implements
22 public void apply(Project project) {
21 public void apply(Project project) {
23 ProjectLayout layout = project.getLayout();
22 ProjectLayout layout = project.getLayout();
24
23
25 project.getPluginManager().apply(ContainerBasePlugin.class);
24 var basePlugin = plugin(ContainerBasePlugin.class);
26
25 var containerExtension = basePlugin.getContainerExtension();
27 var basePlugin = project.getPlugins().findPlugin(ContainerBasePlugin.class);
28 if (basePlugin == null)
29 throw new GradleException("The container-base plugin fails to be applied");
30
31 var containerExtension = basePlugin.getContainerExtension();
32
26
33 project.getConfigurations().create(Dependency.DEFAULT_CONFIGURATION, c -> {
27 configuration(Dependency.DEFAULT_CONFIGURATION, Configurations.CONSUMABLE);
34 c.setCanBeConsumed(true);
28 configuration(ARCHIVE_CONFIGURATION, Configurations.RESOLVABLE);
35 c.setCanBeResolved(false);
36 });
37
29
38 project.getConfigurations().create(ARCHIVE_CONFIGURATION, c -> {
30 var processResourcesTask = task("processResources", Copy.class, t -> {
39 c.setCanBeConsumed(true);
40 c.setCanBeResolved(false);
41 });
42
43 TaskProvider<Copy> processResourcesTask = project.getTasks().register("processResources", Copy.class, t -> {
44 t.setGroup(BUILD_GROUP);
31 t.setGroup(BUILD_GROUP);
45 t.from(layout.getProjectDirectory().dir("src/main"));
32 t.from(layout.getProjectDirectory().dir("src/main"));
46 t.into(containerExtension.getContextDirectory());
33 t.into(containerExtension.getContextDirectory());
47 });
34 });
48
35
49 TaskProvider<BuildImage> buildImageTask = project.getTasks().register("buildImage", BuildImage.class, t -> {
36 var buildImageTask = task("buildImage", BuildImage.class, t -> {
50 t.setGroup(BUILD_GROUP);
37 t.setGroup(BUILD_GROUP);
51 t.dependsOn(processResourcesTask);
38 t.dependsOn(processResourcesTask);
52
39
@@ -56,21 +43,21 public class ContainerPlugin implements
56 t.getImageName().set(containerExtension.getImageName().map(ImageName::toString));
43 t.getImageName().set(containerExtension.getImageName().map(ImageName::toString));
57 });
44 });
58
45
59 project.getTasks().register("clean", Delete.class, t -> {
46 task("clean", Delete.class, t -> {
60 t.delete(layout.getBuildDirectory());
47 t.delete(layout.getBuildDirectory());
61 });
48 });
62
49
63 project.getTasks().register("build", t -> {
50 task("build", DefaultTask.class, t -> {
64 t.setGroup(BUILD_GROUP);
51 t.setGroup(BUILD_GROUP);
65 t.dependsOn(buildImageTask);
52 t.dependsOn(buildImageTask);
66 });
53 });
67
54
68 project.getTasks().register("pushImage", PushImage.class, t -> {
55 task("pushImage", PushImage.class, t -> {
69 t.dependsOn(buildImageTask);
56 t.dependsOn(buildImageTask);
70 t.getImageName().set(buildImageTask.flatMap(BuildImage::getImageName));
57 t.getImageName().set(buildImageTask.flatMap(BuildImage::getImageName));
71 });
58 });
72
59
73 project.getTasks().register("saveImage", SaveImage.class, t -> {
60 task("saveImage", SaveImage.class, t -> {
74 t.dependsOn(buildImageTask);
61 t.dependsOn(buildImageTask);
75 t.getExportImages().add(buildImageTask.flatMap(BuildImage::getImageName));
62 t.getExportImages().add(buildImageTask.flatMap(BuildImage::getImageName));
76 });
63 });
@@ -36,7 +36,7 public interface ProjectMixin {
36 return getProject().getLayout().getProjectDirectory();
36 return getProject().getLayout().getProjectDirectory();
37 }
37 }
38
38
39 /** Applies and returns the specified plugin, plugin is applied only once. */
39 /** Applies and returns the specified plugin, the plugin is applied only once. */
40 default <T extends Plugin<Project>> T plugin(Class<T> clazz) {
40 default <T extends Plugin<Project>> T plugin(Class<T> clazz) {
41 getProject().getPluginManager().apply(clazz);
41 getProject().getPluginManager().apply(clazz);
42 return getProject().getPlugins().findPlugin(clazz);
42 return getProject().getPlugins().findPlugin(clazz);
@@ -3,160 +3,105 package org.implab.gradle.containers.cli
3 import java.io.File;
3 import java.io.File;
4 import java.io.IOException;
4 import java.io.IOException;
5 import java.util.List;
5 import java.util.List;
6 import java.util.Optional;
7 import java.util.Set;
6 import java.util.Set;
8 import java.util.concurrent.ExecutionException;
7 import java.util.concurrent.ExecutionException;
9
8
10 import org.gradle.api.logging.Logger;
11
12 public abstract class DockerTraits {
9 public abstract class DockerTraits {
13
10
14 public final String BUILD_COMMAND = "build";
11 private final String BUILD_COMMAND = "build";
15 public final String PUSH_COMMAND = "push";
12 private final String PUSH_COMMAND = "push";
16 public final String RUN_COMMAND = "run";
13 private final String RUN_COMMAND = "run";
17 public final String SAVE_COMMAND = "save";
14 private final String EXEC_COMMAND = "exec";
18 public final String INSPECT_COMMAND = "inspect";
15 private final String SAVE_COMMAND = "save";
19 public final String IMAGE_COMMAND = "image";
16 private final String INSPECT_COMMAND = "inspect";
20 public final String TAG_COMMAND = "tag";
17 private final String IMAGE_COMMAND = "image";
21 public final String COMPOSE_COMMAND = "compose";
18 private final String TAG_COMMAND = "tag";
22 public final String UP_COMMAND = "up";
19 private final String COMPOSE_COMMAND = "compose";
23 public final String STOP_COMMAND = "stop";
20 private final String STOP_COMMAND = "stop";
24 public final String RM_COMMAND = "rm";
21 private final String START_COMMAND = "start";
25
22 private final String CONTAINER_COMMAND = "container";
26 public abstract Logger getLogger();
23 private final String RM_COMMAND = "rm";
27
24
28 public abstract Optional<File> getWorkingDir();
25 protected abstract ProcessSpec builder(String... command);
29
30 public abstract String getCliCmd();
31
26
32 protected boolean checkRetCode(ProcessSpec proc, int code)
27 public ProcessSpec imageBuild(String imageName, File contextDirectory, List<String> options) {
33 throws InterruptedException, ExecutionException, IOException {
28 return builder(BUILD_COMMAND)
34 if (getLogger().isInfoEnabled()) {
29 .args(options)
35 proc.redirectStdout(RedirectTo.consumer(getLogger()::info))
30 .args("-t", imageName, contextDirectory.getAbsolutePath());
36 .redirectStderr(RedirectTo.consumer(getLogger()::info));
37 }
38
39 getLogger().info("Starting: {}", proc.command());
40
41 return proc.start().get() == code;
42 }
31 }
43
32
44 protected void exec(ProcessSpec proc) throws InterruptedException, IOException, ExecutionException {
33 public ProcessSpec imagePush(String image, List<String> options) {
45 if (getLogger().isInfoEnabled())
34 return builder(PUSH_COMMAND)
46 proc.redirectStdout(RedirectTo.consumer(getLogger()::info));
35 .args(options)
47
36 .args(image);
48 if (getLogger().isErrorEnabled())
49 proc.redirectStderr(RedirectTo.consumer(getLogger()::error));
50
51 getLogger().info("Starting: {}", proc.command());
52
53 var code = proc.start().get();
54 if (code != 0) {
55 getLogger().error("The process exited with code {}", code);
56 throw new IOException("The process exited with error code " + code);
57 }
58 }
37 }
59
38
60 protected ProcessSpec builder(String... args) {
39 public ProcessSpec containerRun(String image, List<String> options, List<String> command) {
61 var spec = new ProcessSpec().args(getCliCmd()).args(args);
62
63 getWorkingDir().ifPresent(spec::directory);
64
65 return spec;
66 }
67
68 public void buildImage(String imageName, File contextDirectory, List<String> options)
69 throws IOException, InterruptedException, ExecutionException {
70 var spec = builder(BUILD_COMMAND)
71 .args(options)
72 .args("-t", imageName, contextDirectory.getAbsolutePath());
73
74 exec(spec);
75 }
76
77 public void pushImage(String image, List<String> options)
78 throws InterruptedException, IOException, ExecutionException {
79 var spec = builder(PUSH_COMMAND)
80 .args(options)
81 .args(image);
82
83 exec(spec);
84 }
85
86 public ProcessSpec runImage(String image, List<String> options, List<String> command)
87 throws InterruptedException, IOException {
88 return builder(RUN_COMMAND)
40 return builder(RUN_COMMAND)
89 .args(options)
41 .args(options)
90 .args(image)
42 .args(image)
91 .args(command);
43 .args(command);
92 }
44 }
93
45
94 public void stopContainer(String containerId, List<String> options) throws InterruptedException, IOException, ExecutionException {
46 public ProcessSpec containerExec(String containerId, List<String> options, List<String> command) {
95 exec(builder(STOP_COMMAND, containerId).args(options));
47 return builder(EXEC_COMMAND)
48 .args(options)
49 .args(containerId)
50 .args(command);
96 }
51 }
97
52
98 public void saveImage(Set<String> images, File output)
53 public ProcessSpec containerStop(String containerId, List<String> options) {
99 throws InterruptedException, IOException, ExecutionException {
54 return builder(STOP_COMMAND, containerId).args(options);
100 if (output.exists())
101 output.delete();
102
103 var spec = builder(SAVE_COMMAND)
104 .args("-o", output.getAbsolutePath())
105 .args(images);
106
107 exec(spec);
108 }
109
110 public void tagImage(String source, String target) throws InterruptedException, IOException, ExecutionException {
111 exec(builder(TAG_COMMAND, source, target));
112 }
55 }
113
56
114 public boolean imageExists(String imageId) throws InterruptedException, IOException, ExecutionException {
57 public ProcessSpec containerStart(Set<String> containers, List<String> options) {
115 getLogger().info("Check image {} exists", imageId);
58 return builder(START_COMMAND).args(options).args(containers);
116
117 return checkRetCode(
118 builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId),
119 0);
120 }
59 }
121
60
122 public boolean imageExists(File imageIdFile) {
61 public ProcessSpec containerRm(Set<String> containers, boolean removeVolumes, boolean force) {
123 if (imageIdFile.exists()) {
62 var spec = builder(RM_COMMAND);
124 try {
125 var imageId = Utils.readImageRef(imageIdFile);
126 return imageExists(imageId.getId());
127 } catch (IOException | InterruptedException | ExecutionException e) {
128 getLogger().error("Failed to read imageId {}: {}", imageIdFile, e);
129 return false;
130 }
131 }
132 return false;
133 }
134
63
135 ProcessSpec compose(File primaryCompose, Set<String> profiles, String... extra) {
64 if(removeVolumes)
136 var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath());
65 spec.args("--volumes");
66 if (force)
67 spec.args("--force");
137
68
138 for (var profile : profiles)
69 spec.args(containers);
139 spec.args("--profile", profile);
140
141 spec.args(extra);
142
70
143 return spec;
71 return spec;
144 }
72 }
145
73
146 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
74 public ProcessSpec imageSave(Set<String> images, File output) {
147 exec(compose(primaryCompose, profiles, UP_COMMAND, "--detach"));
75 if (output.exists())
76 output.delete();
77
78 return builder(SAVE_COMMAND)
79 .args("-o", output.getAbsolutePath())
80 .args(images);
148 }
81 }
149
82
150 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
83 public ProcessSpec imageTag(String source, String target) {
151 exec(compose(primaryCompose, profiles, STOP_COMMAND));
84 return builder(TAG_COMMAND, source, target);
85 }
86
87 public ProcessSpec imageExists(String imageId) throws InterruptedException, IOException, ExecutionException {
88 return builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId);
152 }
89 }
153
90
154 public void composeRm(File primaryCompose, Set<String> profiles, boolean removeVolumes)
91 public ProcessSpec containerExists(String imageId) throws InterruptedException, IOException, ExecutionException {
155 throws InterruptedException, IOException, ExecutionException {
92 return builder(CONTAINER_COMMAND, INSPECT_COMMAND, "--format", "container-exists", imageId);
156 var spec = compose(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
93 }
157 if (removeVolumes)
158 spec.args("--volumes");
159
94
160 exec(spec);
95 public ComposeTraits compose(File primaryCompose, Set<String> profiles) {
96 return new ComposeTraits() {
97 @Override
98 public ProcessSpec compose(String command) {
99 var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath());
100
101 for (var profile : profiles)
102 spec.args("--profile", profile);
103 return spec.args(command);
104 }
105 };
161 }
106 }
162 }
107 }
@@ -4,7 +4,6 import java.io.File;
4 import java.io.FileInputStream;
4 import java.io.FileInputStream;
5 import java.io.InputStream;
5 import java.io.InputStream;
6 import java.io.OutputStream;
6 import java.io.OutputStream;
7 import java.util.concurrent.Callable;
8 import java.util.concurrent.CompletableFuture;
7 import java.util.concurrent.CompletableFuture;
9
8
10 public interface RedirectFrom {
9 public interface RedirectFrom {
@@ -18,6 +18,9 import java.util.stream.StreamSupport;
18 import java.nio.file.Files;
18 import java.nio.file.Files;
19 import java.util.List;
19 import java.util.List;
20 import org.gradle.api.Action;
20 import org.gradle.api.Action;
21 import org.gradle.api.GradleException;
22 import org.gradle.api.file.Directory;
23 import org.gradle.api.file.FileSystemLocation;
21 import org.gradle.internal.impldep.org.bouncycastle.util.Iterable;
24 import org.gradle.internal.impldep.org.bouncycastle.util.Iterable;
22
25
23 import com.fasterxml.jackson.core.exc.StreamWriteException;
26 import com.fasterxml.jackson.core.exc.StreamWriteException;
@@ -96,19 +99,17 public final class Utils {
96 }
99 }
97 }
100 }
98
101
99 public static String readAll(final File src) throws IOException {
102 public static String readAll(final File src) {
100 return Files.readString(src.toPath());
103 try {
104 return src.isFile() ? Files.readString(src.toPath()) : null;
105 } catch (IOException e) {
106 throw new GradleException("Failed to read file " + src.toString(), e);
107 }
101 }
108 }
102
109
103 public static List<String> readAll(final Iterable<? extends File> files) {
110 public static List<String> readAll(final Iterable<? extends File> files) {
104 return StreamSupport.stream(files.spliterator(), false)
111 return StreamSupport.stream(files.spliterator(), false)
105 .map(file -> {
112 .map(Utils::readAll)
106 try {
107 return Utils.readAll(file);
108 } catch (IOException e) {
109 throw new RuntimeException(e);
110 }
111 })
112 .toList();
113 .toList();
113 }
114 }
114
115
@@ -153,4 +154,17 public final class Utils {
153 return set.stream().map(Object::toString).collect(Collectors.toSet());
154 return set.stream().map(Object::toString).collect(Collectors.toSet());
154 }
155 }
155
156
157 public static File normalizeFile(Object file) {
158 if (file == null)
159 throw new IllegalArgumentException("file");
160
161 if (file instanceof String)
162 return new File((String)file);
163 if (file instanceof FileSystemLocation)
164 return ((FileSystemLocation)file).getAsFile();
165 else if (file instanceof File)
166 return (File)file;
167 throw new ClassCastException();
168 }
169
156 } No newline at end of file
170 }
@@ -4,6 +4,7 import java.util.concurrent.Callable;
4
4
5 import org.gradle.api.provider.ListProperty;
5 import org.gradle.api.provider.ListProperty;
6 import org.gradle.api.tasks.Input;
6 import org.gradle.api.tasks.Input;
7 import org.gradle.api.tasks.Optional;
7 import org.implab.gradle.containers.PropertiesMixin;
8 import org.implab.gradle.containers.PropertiesMixin;
8
9
9 import groovy.lang.Closure;
10 import groovy.lang.Closure;
@@ -11,6 +12,7 import groovy.lang.Closure;
11 public interface OptionsMixin extends PropertiesMixin {
12 public interface OptionsMixin extends PropertiesMixin {
12
13
13 @Input
14 @Input
15 @Optional
14 ListProperty<String> getOptions();
16 ListProperty<String> getOptions();
15
17
16 default void option(String opt) {
18 default void option(String opt) {
@@ -2,8 +2,8 package org.implab.gradle.containers.dsl
2
2
3 import java.util.ArrayList;
3 import java.util.ArrayList;
4
4
5 import org.gradle.api.provider.ListProperty;
6 import org.gradle.api.provider.Property;
5 import org.gradle.api.provider.Property;
6 import org.gradle.api.provider.SetProperty;
7
7
8 public abstract class VolumeSpec {
8 public abstract class VolumeSpec {
9
9
@@ -11,7 +11,7 public abstract class VolumeSpec {
11
11
12 public abstract Property<String> getTarget();
12 public abstract Property<String> getTarget();
13
13
14 public abstract ListProperty<String> getOptions();
14 public abstract SetProperty<String> getOptions();
15
15
16 public void ro() {
16 public void ro() {
17 getOptions().add("ro");
17 getOptions().add("ro");
@@ -21,6 +21,7 import org.gradle.api.tasks.OutputFile;
21 import org.gradle.api.tasks.SkipWhenEmpty;
21 import org.gradle.api.tasks.SkipWhenEmpty;
22 import org.gradle.api.tasks.TaskAction;
22 import org.gradle.api.tasks.TaskAction;
23 import java.io.File;
23 import java.io.File;
24 import java.io.IOException;
24
25
25 import org.implab.gradle.containers.cli.ImageRef;
26 import org.implab.gradle.containers.cli.ImageRef;
26 import org.implab.gradle.containers.cli.Utils;
27 import org.implab.gradle.containers.cli.Utils;
@@ -55,8 +56,7 public abstract class BuildImage extends
55
56
56 public BuildImage() {
57 public BuildImage() {
57 getOutputs().upToDateWhen(task -> getImageIdFile()
58 getOutputs().upToDateWhen(task -> getImageIdFile()
58 .map(RegularFile::getAsFile)
59 .map(this::imageExists)
59 .map(docker()::imageExists)
60 .getOrElse(false));
60 .getOrElse(false));
61 }
61 }
62
62
@@ -77,12 +77,26 public abstract class BuildImage extends
77 return new MapPropertyEntry<String, String>(getBuildArgs(), key, getProviders());
77 return new MapPropertyEntry<String, String>(getBuildArgs(), key, getProviders());
78 }
78 }
79
79
80 private boolean imageExists(RegularFile file) {
81 return readRefId(file.getAsFile()).map(this::imageExists).orElse(false);
82 }
83
84 private java.util.Optional<String> readRefId(File idFile) {
85 try {
86 return idFile.exists() ? java.util.Optional.of(Utils.readImageRef(idFile).getId())
87 : java.util.Optional.empty();
88 } catch (IOException e) {
89 getLogger().error("Failed to read imageId {}: {}", idFile, e);
90 return java.util.Optional.empty();
91 }
92 }
93
80 @TaskAction
94 @TaskAction
81 public void run() throws Exception {
95 public void run() throws Exception {
82 List<String> args = new ArrayList<>();
96 List<String> args = new ArrayList<>();
83
97
84 // create a temp file to store image id
98 // create a temp file to store image id
85 var iidFile = new File(this.getTemporaryDir(), "iid");
99 var iidFile = new File(this.getTemporaryDir(), "imageid");
86
100
87 // specify where to write image id
101 // specify where to write image id
88 args.addAll(List.of("--iidfile", iidFile.getAbsolutePath()));
102 args.addAll(List.of("--iidfile", iidFile.getAbsolutePath()));
@@ -104,11 +118,13 public abstract class BuildImage extends
104 var imageTag = getImageName().map(Object::toString).get();
118 var imageTag = getImageName().map(Object::toString).get();
105
119
106 // build image
120 // build image
107 docker().buildImage(
121 var spec = docker().imageBuild(
108 imageTag,
122 imageTag,
109 getContextDirectory().map(Directory::getAsFile).get(),
123 getContextDirectory().map(Directory::getAsFile).get(),
110 args);
124 args);
111
125
126 exec(spec);
127
112 // read the built image id and store image ref metadata
128 // read the built image id and store image ref metadata
113 var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile));
129 var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile));
114 Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef);
130 Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef);
@@ -1,10 +1,31
1 package org.implab.gradle.containers.tasks;
1 package org.implab.gradle.containers.tasks;
2
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.concurrent.ExecutionException;
6
3 import org.gradle.api.provider.Property;
7 import org.gradle.api.provider.Property;
4 import org.gradle.api.tasks.Internal;
8 import org.gradle.api.tasks.Internal;
9 import org.gradle.api.tasks.TaskAction;
5
10
6 public abstract class ComposeExec {
11 public abstract class ComposeExec extends BaseExecTask implements ComposeTaskMixin {
7
12
8 @Internal
13 @Internal
9 public abstract Property<String> getServiceName();
14 public abstract Property<String> getServiceName();
15
16 public ComposeExec() {
17 applyComposeConvention();
18 }
19
20 @TaskAction
21 public void run() throws InterruptedException, ExecutionException, IOException {
22 var params = new ArrayList<>(getOptions().get());
23
24 if (isDetached())
25 params.add("--detach");
26 if (!getAllocateTty())
27 params.add("--no-TTY");
28
29 exec(docker().compose(this).exec(getServiceName().get(), params, getCommandLine().get()));
30 }
10 }
31 }
@@ -7,17 +7,18 import org.gradle.api.provider.Property;
7 import org.gradle.api.tasks.Internal;
7 import org.gradle.api.tasks.Internal;
8 import org.gradle.api.tasks.TaskAction;
8 import org.gradle.api.tasks.TaskAction;
9
9
10 public abstract class ComposeRm extends ComposeTask {
10 public abstract class ComposeRm extends DockerCliTask implements ComposeTaskMixin {
11
11
12 @Internal
12 @Internal
13 public abstract Property<Boolean> getRemoveVolumes();
13 public abstract Property<Boolean> getRemoveVolumes();
14
14
15 public ComposeRm() {
15 public ComposeRm() {
16 applyComposeConvention();
16 getRemoveVolumes().convention(true);
17 getRemoveVolumes().convention(true);
17 }
18 }
18
19
19 @TaskAction
20 @TaskAction
20 public void run() throws InterruptedException, IOException, ExecutionException {
21 public void run() throws InterruptedException, IOException, ExecutionException {
21 docker().composeRm(getComposeFile().get().getAsFile(), getProfiles().get(), getRemoveVolumes().get());
22 exec(docker().compose(this).rm(getRemoveVolumes().get()));
22 }
23 }
23 }
24 }
@@ -5,10 +5,14 import java.util.concurrent.ExecutionExc
5
5
6 import org.gradle.api.tasks.TaskAction;
6 import org.gradle.api.tasks.TaskAction;
7
7
8 public abstract class ComposeStop extends ComposeTask {
8 public abstract class ComposeStop extends DockerCliTask implements ComposeTaskMixin {
9
10 public ComposeStop() {
11 applyComposeConvention();
12 }
9
13
10 @TaskAction
14 @TaskAction
11 public void run() throws InterruptedException, IOException, ExecutionException {
15 public void run() throws InterruptedException, IOException, ExecutionException {
12 docker().composeStop(getComposeFile().getAsFile().get(), getProfiles().get());
16 exec(docker().compose(this).stop());
13 }
17 }
14 } No newline at end of file
18 }
@@ -1,33 +1,37
1 package org.implab.gradle.containers.tasks;
1 package org.implab.gradle.containers.tasks;
2
2
3 import java.io.File;
4 import java.util.Optional;
5
6 import org.gradle.api.file.RegularFile;
3 import org.gradle.api.file.RegularFileProperty;
7 import org.gradle.api.file.RegularFileProperty;
4 import org.gradle.api.provider.SetProperty;
8 import org.gradle.api.provider.SetProperty;
5 import org.gradle.api.tasks.Internal;
9 import org.gradle.api.tasks.Internal;
10 import org.implab.gradle.containers.ComposeExtension;
6
11
7 /**
12 /**
8 * Base task for compose subtasks like 'uo', 'rm', 'stop', etc.
13 * Base task for compose subtasks like 'uo', 'rm', 'stop', etc.
9 */
14 */
10 public abstract class ComposeTask extends DockerCliTask {
15 public interface ComposeTaskMixin extends TaskServices {
11
16
12 /** The list of profiles to use with the compose commands */
17 /** The list of profiles to use with the compose commands */
13 @Internal
18 @Internal
14 public abstract SetProperty<String> getProfiles();
19 SetProperty<String> getProfiles();
15
20
16 /**
21 /**
17 * The primary compose files. This task is executed regardless whether these
22 * The primary compose files. This task is executed regardless whether these
18 * files was changed.
23 * files was changed.
19 */
24 */
20 @Internal
25 @Internal
21 public abstract RegularFileProperty getComposeFile();
26 RegularFileProperty getComposeFile();
22
27
23 protected ComposeTask() {
28 default void applyComposeConvention() {
24 // the task can only be evaluated if the compose file is present
29 onlyIf(task -> getComposeFile().map(RegularFile::getAsFile).map(File::exists).getOrElse(false));
25 setOnlyIf(self -> {
30 Optional.ofNullable(getProject().getExtensions().findByType(ComposeExtension.class))
26 var composeFile = getComposeFile().get().getAsFile();
31 .ifPresent(ext -> {
27 var exists = composeFile.exists();
32 getProfiles().set(ext.getProfiles());
28 getLogger().info("file: {} {}", composeFile.toString(), exists ? "exists" : "doesn't exist");
33 getComposeFile().set(ext.getComposeFile());
29 return exists;
34 });
30 });
35
31 }
36 }
32
33 }
37 }
@@ -3,12 +3,29 package org.implab.gradle.containers.tas
3 import java.io.IOException;
3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 import java.util.concurrent.ExecutionException;
5
5
6 import org.gradle.api.tasks.Internal;
6 import org.gradle.api.tasks.TaskAction;
7 import org.gradle.api.tasks.TaskAction;
7
8
8 public abstract class ComposeUp extends ComposeTask {
9 public abstract class ComposeUp extends DockerCliTask implements ComposeTaskMixin {
10
11 private boolean detached = true;
12
13 @Internal
14 public boolean isDetached() {
15 return detached;
16 }
17
18 public void setDetached(boolean value) {
19 detached = value;
20 }
21
22 public ComposeUp() {
23 applyComposeConvention();
24 }
9
25
10 @TaskAction
26 @TaskAction
11 public void run() throws InterruptedException, IOException, ExecutionException {
27 public void run() throws InterruptedException, IOException, ExecutionException {
12 docker().composeUp(getComposeFile().getAsFile().get(), getProfiles().get());
28
29 exec(docker().compose(this).up(isDetached()));
13 }
30 }
14 }
31 }
@@ -1,19 +1,24
1 package org.implab.gradle.containers.tasks;
1 package org.implab.gradle.containers.tasks;
2
2
3 import java.io.File;
3 import java.io.File;
4 import java.io.IOException;
4 import java.util.Optional;
5 import java.util.Optional;
6 import java.util.concurrent.ExecutionException;
5
7
6 import org.gradle.api.DefaultTask;
8 import org.gradle.api.DefaultTask;
7 import org.gradle.api.logging.Logger;
9 import org.gradle.api.GradleException;
10 import org.gradle.api.file.RegularFile;
8 import org.gradle.api.provider.Property;
11 import org.gradle.api.provider.Property;
9 import org.gradle.api.tasks.Input;
12 import org.gradle.api.tasks.Input;
10 import org.gradle.api.tasks.Internal;
13 import org.gradle.api.tasks.Internal;
11 import org.implab.gradle.containers.ContainerExtension;
14 import org.implab.gradle.containers.ContainerExtension;
12 import org.implab.gradle.containers.PropertiesMixin;
15 import org.implab.gradle.containers.PropertiesMixin;
16 import org.implab.gradle.containers.cli.ComposeTraits;
13 import org.implab.gradle.containers.cli.DockerTraits;
17 import org.implab.gradle.containers.cli.DockerTraits;
14 import org.implab.gradle.containers.cli.ProcessSpec;
18 import org.implab.gradle.containers.cli.ProcessSpec;
15 import org.implab.gradle.containers.cli.RedirectFrom;
19 import org.implab.gradle.containers.cli.RedirectFrom;
16 import org.implab.gradle.containers.cli.RedirectTo;
20 import org.implab.gradle.containers.cli.RedirectTo;
21 import org.implab.gradle.containers.cli.Utils;
17
22
18 public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin {
23 public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin {
19
24
@@ -45,44 +50,100 public abstract class DockerCliTask exte
45 return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty();
50 return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty();
46 }
51 }
47
52
48 protected Optional<RedirectTo> getStdoutRedirection() {
53 protected Optional<RedirectTo> stdoutRedirection() {
49 return loggerInfoRedirect();
54 return loggerInfoRedirect();
50 }
55 }
51
56
52 protected Optional<RedirectTo> getStderrRedirection() {
57 protected Optional<RedirectTo> stderrRedirection() {
53 return loggerErrorRedirect();
58 return loggerErrorRedirect();
54 }
59 }
55
60
56 protected Optional<RedirectFrom> getStdinRedirection() {
61 protected Optional<RedirectFrom> stdinRedirection() {
57 return Optional.empty();
62 return Optional.empty();
58 }
63 }
59
64
60 protected DockerTraits docker() {
65 protected TaskDockerTraits docker() {
61 return new TaskDockerTraits();
66 return new TaskDockerTraits();
62 }
67 }
63
68
64 protected void exec(ProcessSpec spec) {
69 protected boolean imageExists(String imageId) {
70 try {
71 return checkRetCode(docker().imageExists(imageId), 0);
72 } catch (InterruptedException | ExecutionException | IOException e) {
73 // wrap to unchecked exception
74 throw new GradleException("Failed to execute imageExists", e);
75 }
76 }
77
78 protected boolean containerExists(String containerId) {
79 try {
80 return checkRetCode(docker().containerExists(containerId), 0);
81 } catch (InterruptedException | ExecutionException | IOException e) {
82 // wrap to unchecked exception
83 throw new GradleException("Failed to execute imageExists", e);
84 }
85 }
86
87 protected void exec(ProcessSpec spec) throws InterruptedException, ExecutionException, IOException {
88
89 stdoutRedirection().ifPresent(spec::redirectStdout);
90 stderrRedirection().ifPresent(spec::redirectStderr);
91 stdinRedirection().ifPresent(spec::redirectStdin);
92
93 getLogger().info("Staring: {}", spec.command());
94
95 // runs the command and checks the error code
96 spec.exec();
97 }
65
98
66 getLogger().info("Starting: {}", spec.command());
99 protected boolean checkRetCode(ProcessSpec proc, int code)
100 throws InterruptedException, ExecutionException, IOException {
101 if (getLogger().isInfoEnabled()) {
102 proc.redirectStdout(RedirectTo.consumer(getLogger()::info))
103 .redirectStderr(RedirectTo.consumer(getLogger()::info));
104 }
105
106 getLogger().info("Starting: {}", proc.command());
107
108 return proc.start().get() == code;
109 }
110
111 /**
112 * Helper function to read
113 * @param idFile
114 * @return
115 */
116 protected String readId(File idFile) {
117 if (idFile.exists()) {
118 try {
119 return Utils.readImageRef(idFile).getId();
120 } catch (IOException e) {
121 getLogger().error("Failed to read imageId {}: {}", idFile, e);
122 return null;
123 }
124 } else {
125 return null;
126 }
127 }
128
129 protected ProcessSpec commandBuilder(String... command) {
130 var spec = new ProcessSpec().args(getCliCmd().get()).args(command);
131
132 if (getWorkingDirectory().isPresent())
133 spec.directory(getWorkingDirectory().get());
134
135 return spec;
67 }
136 }
68
137
69 class TaskDockerTraits extends DockerTraits {
138 class TaskDockerTraits extends DockerTraits {
70
139
71 @Override
140 @Override
72 public Logger getLogger() {
141 protected ProcessSpec builder(String... command) {
73 return DockerCliTask.this.getLogger();
142 return commandBuilder(command);
74 }
143 }
75
144
76 @Override
145 public ComposeTraits compose(ComposeTaskMixin props) {
77 public Optional<File> getWorkingDir() {
146 return compose(props.getComposeFile().map(RegularFile::getAsFile).get(), props.getProfiles().get());
78 return getWorkingDirectory()
79 .map(Optional::of)
80 .getOrElse(Optional.empty());
81 }
82
83 @Override
84 public String getCliCmd() {
85 return DockerCliTask.this.getCliCmd().get();
86 }
147 }
87
148
88 }
149 }
@@ -3,7 +3,6 package org.implab.gradle.containers.tas
3 import java.io.IOException;
3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 import java.util.concurrent.ExecutionException;
5
5
6 import org.gradle.api.provider.ListProperty;
7 import org.gradle.api.provider.Property;
6 import org.gradle.api.provider.Property;
8 import org.gradle.api.tasks.Input;
7 import org.gradle.api.tasks.Input;
9 import org.gradle.api.tasks.Optional;
8 import org.gradle.api.tasks.Optional;
@@ -19,15 +18,11 public abstract class PushImage extends
19 @Optional
18 @Optional
20 public abstract Property<Object> getTransport();
19 public abstract Property<Object> getTransport();
21
20
22 @Input
23 @Optional
24 public abstract ListProperty<String> getOptions();
25
26 @TaskAction
21 @TaskAction
27 public void run() throws InterruptedException, IOException, ExecutionException {
22 public void run() throws InterruptedException, IOException, ExecutionException {
28
23
29 docker().pushImage(
24 exec(docker().imagePush(
30 getImageName().map(Object::toString).get(),
25 getImageName().map(Object::toString).get(),
31 getOptions().get());
26 getOptions().get()));
32 }
27 }
33 }
28 }
@@ -4,57 +4,32 import java.io.File;
4 import java.io.IOException;
4 import java.io.IOException;
5 import java.util.ArrayList;
5 import java.util.ArrayList;
6 import java.util.List;
6 import java.util.List;
7 import java.util.Optional;
7 import java.util.Set;
8 import java.util.concurrent.ExecutionException;
8 import java.util.concurrent.ExecutionException;
9
9
10 import org.gradle.api.Action;
10 import org.gradle.api.Action;
11 import org.gradle.api.GradleException;
11 import org.gradle.api.file.RegularFileProperty;
12 import org.gradle.api.file.RegularFileProperty;
12 import org.gradle.api.provider.ListProperty;
13 import org.gradle.api.provider.Property;
13 import org.gradle.api.provider.Property;
14 import org.gradle.api.tasks.Internal;
14 import org.gradle.api.tasks.Internal;
15 import org.gradle.api.tasks.TaskAction;
15 import org.gradle.api.tasks.TaskAction;
16 import org.implab.gradle.containers.cli.RedirectFrom;
17 import org.implab.gradle.containers.cli.RedirectTo;
18 import org.implab.gradle.containers.dsl.OptionsMixin;
19 import org.implab.gradle.containers.dsl.RedirectFromSpec;
20 import org.implab.gradle.containers.dsl.RedirectToSpec;
21 import org.implab.gradle.containers.dsl.VolumeSpec;
16 import org.implab.gradle.containers.dsl.VolumeSpec;
22
17
23 public abstract class RunContainer extends DockerCliTask implements OptionsMixin {
18 public abstract class RunContainer extends BaseExecTask implements ContainerTaskMixin {
24
25 private final RedirectToSpec redirectStderr = new RedirectToSpec();
26
27 private final RedirectToSpec redirectStdout = new RedirectToSpec();
28
29 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
30
19
31 private boolean transientContainer = true;
20 private boolean transientContainer = true;
32
21
33 private boolean detached = false;
22 private boolean startIfExists = true;
34
35 private boolean interactive = false;
36
23
37 @Internal
24 @Internal
38 public abstract Property<Object> getImageName();
25 public abstract Property<Object> getImageName();
39
26
40 @Internal
27 @Internal
41 public abstract ListProperty<String> getCommandLine();
28 public abstract Property<String> getContainerName();
42
43 @Internal
44 public RedirectFromSpec getStdin() {
45 return redirectStdin;
46 }
47
29
48 @Internal
30 /**
49 public RedirectToSpec getStdout() {
31 * Transient containers will be removed after completion. Default is `true`
50 return redirectStdout;
32 */
51 }
52
53 @Internal
54 public RedirectToSpec getStderr() {
55 return redirectStderr;
56 }
57
58 @Internal
33 @Internal
59 public boolean isTransientContainer() {
34 public boolean isTransientContainer() {
60 return transientContainer;
35 return transientContainer;
@@ -65,24 +40,22 public abstract class RunContainer exten
65 }
40 }
66
41
67 @Internal
42 @Internal
68 public boolean isDetached() {
43 public boolean getStartIfExists() {
69 // if IO was redirected the container should run in foreground
44 return startIfExists;
70 if (redirectStdin.isRedirected() || redirectStdout.isRedirected() || redirectStderr.isRedirected())
45 }
71 return false;
72
46
73 return detached;
47 public void setStartIdExists(boolean value) {
48 startIfExists = value;
74 }
49 }
75
50
76 public void setDetached(boolean value) {
51 /**
77 detached = value;
52 * Specified the file where the stared container id will be written.
78 }
53 * Default value is a temporary file.
79
54 *
80 @Internal
55 * <p>
81 public boolean isInteractive() {
56 * This property can be use in tasks where the container id is required
82 // enable interactive mode when processing standard input
57 * to perform operation, for example to StopContainer tasks.
83 return redirectStdin.isRedirected() || interactive;
58 */
84 }
85
86 @Internal
59 @Internal
87 public abstract RegularFileProperty getContainerIdFile();
60 public abstract RegularFileProperty getContainerIdFile();
88
61
@@ -90,6 +63,11 public abstract class RunContainer exten
90 getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid"));
63 getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid"));
91 }
64 }
92
65
66 /**
67 * Adds volume specification
68 *
69 * @param configure
70 */
93 public void volume(Action<VolumeSpec> configure) {
71 public void volume(Action<VolumeSpec> configure) {
94 getOptions().add("-v");
72 getOptions().add("-v");
95 getOptions().add(provider(() -> {
73 getOptions().add(provider(() -> {
@@ -99,61 +77,49 public abstract class RunContainer exten
99 }));
77 }));
100 }
78 }
101
79
102 void commandLine(String... args) {
103 getCommandLine().addAll(args);
104 }
105
106 @Override
107 protected Optional<RedirectTo> getStdoutRedirection() {
108 return redirectStdout.getRedirection().or(super::getStdoutRedirection);
109 }
110
111 @Override
112 protected Optional<RedirectTo> getStderrRedirection() {
113 return redirectStderr.getRedirection().or(super::getStderrRedirection);
114 }
115
116 @Override
117 protected Optional<RedirectFrom> getStdinRedirection() {
118 return redirectStdin.getRedirection();
119 }
120
121 @TaskAction
80 @TaskAction
122 public void run() throws InterruptedException, IOException, ExecutionException {
81 public void run() throws InterruptedException, IOException, ExecutionException {
123 var params = new ArrayList<>(getOptions().get());
82 var params = new ArrayList<>(getOptions().get());
124
83
125 if (isInteractive())
84 if (getContainerId().map(this::containerExists).getOrElse(false)) {
126 params.add("--interactive");
85 var cid = getContainerId().get();
127 if (isTransientContainer())
86 if (getStartIfExists()) {
128 params.add("--rm");
87 getLogger().info("Container {} already created, starting.", cid);
129 if (isDetached())
130 params.add("--detach");
131 if (getContainerIdFile().isPresent()) {
132 getContainerIdFile().getAsFile().get().delete();
133
88
134 params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString()));
89 if (isInteractive())
135 }
90 params.add("--interactive");
91 if (!isDetached())
92 params.add("--attach");
93
94 exec(docker().containerStart(Set.of(cid), params));
95 } else {
96 throw new GradleException("Container " + cid + " already exists");
97 }
98 } else {
136
99
137 var spec = docker().runImage(
100 if (isInteractive())
138 getImageName().map(Object::toString).get(),
101 params.add("--interactive");
139 params,
102 if (isTransientContainer())
140 getCommandLine().get());
103 params.add("--rm");
141
104 if (isDetached())
142 // if process will run in foreground, then setup default redirects
105 params.add("--detach");
143 redirectStdout.getRedirection()
106 if (getAllocateTty())
144 .or(this::loggerInfoRedirect)
107 params.add("--tty");
145 .ifPresent(spec::redirectStdout);
108 if (getContainerName().isPresent())
109 params.addAll(List.of("--name", getContainerName().get()));
146
110
147 redirectStderr.getRedirection()
111 if (getContainerIdFile().isPresent()) {
148 .or(this::loggerErrorRedirect)
112 // delete previous container id otherwise the container will fail to start
149 .ifPresent(spec::redirectStderr);
113 getContainerIdFile().getAsFile().get().delete();
150
114
151 redirectStdin.getRedirection().ifPresent(spec::redirectStdin);
115 params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString()));
116 }
152
117
153 getLogger().info("Staring: {}", spec.command());
118 exec(docker().containerRun(
154
119 getImageName().map(Object::toString).get(),
155 // runs the command and checks the error code
120 params,
156 spec.exec();
121 getCommandLine().get()));
122 }
157 }
123 }
158
124
159 }
125 }
@@ -105,8 +105,8 public abstract class SaveImage extends
105
105
106 @TaskAction
106 @TaskAction
107 public void run() throws InterruptedException, IOException, ExecutionException {
107 public void run() throws InterruptedException, IOException, ExecutionException {
108 docker().saveImage(
108 exec(docker().imageSave(
109 getExportImages().map(Utils::mapToString).get(),
109 getExportImages().map(Utils::mapToString).get(),
110 getArchiveFile().map(RegularFile::getAsFile).get());
110 getArchiveFile().map(RegularFile::getAsFile).get()));
111 }
111 }
112 }
112 }
@@ -5,22 +5,11 import java.util.ArrayList;
5 import java.util.List;
5 import java.util.List;
6 import java.util.concurrent.ExecutionException;
6 import java.util.concurrent.ExecutionException;
7
7
8 import org.gradle.api.GradleException;
9 import org.gradle.api.file.RegularFile;
10 import org.gradle.api.file.RegularFileProperty;
11 import org.gradle.api.provider.Property;
8 import org.gradle.api.provider.Property;
12 import org.gradle.api.provider.Provider;
13 import org.gradle.api.tasks.Internal;
9 import org.gradle.api.tasks.Internal;
14 import org.gradle.api.tasks.TaskAction;
10 import org.gradle.api.tasks.TaskAction;
15 import org.implab.gradle.containers.cli.Utils;
16
11
17 public abstract class StopContainer extends DockerCliTask {
12 public abstract class StopContainer extends DockerCliTask implements ContainerTaskMixin {
18
19 @Internal
20 public abstract RegularFileProperty getContainerIdFile();
21
22 @Internal
23 public abstract Property<String> getContainerName();
24
13
25 @Internal
14 @Internal
26 public abstract Property<String> getStopSignal();
15 public abstract Property<String> getStopSignal();
@@ -28,27 +17,12 public abstract class StopContainer exte
28 @Internal
17 @Internal
29 public abstract Property<Integer> getStopTimeout();
18 public abstract Property<Integer> getStopTimeout();
30
19
31 Provider<String> readCid(RegularFile regularFile) {
20 public StopContainer() {
32 var file = regularFile.getAsFile();
21 onlyIfReason("Container doesn't exists", self -> getContainerId().map(this::containerExists).getOrElse(false));
33
34 return provider(() -> {
35 try {
36 return file.isFile() ? Utils.readAll(file) : null;
37 } catch (IOException e) {
38 throw new GradleException("Failed to read container id from file " + file.toString(), e);
39 }
40 });
41 }
22 }
42
23
43 @TaskAction
24 @TaskAction
44 public void run() throws InterruptedException, IOException, ExecutionException {
25 public void run() throws InterruptedException, IOException, ExecutionException {
45 var cid = getContainerIdFile().flatMap(this::readCid).orElse(getContainerName());
46
47 if (!cid.isPresent()) {
48 getLogger().info("The container name or id hasn't been specified");
49 return;
50 }
51
52 var options = new ArrayList<String>();
26 var options = new ArrayList<String>();
53
27
54 if (getStopSignal().isPresent())
28 if (getStopSignal().isPresent())
@@ -57,10 +31,7 public abstract class StopContainer exte
57 if (getStopTimeout().isPresent())
31 if (getStopTimeout().isPresent())
58 options.addAll(List.of("--time", getStopTimeout().map(Object::toString).get()));
32 options.addAll(List.of("--time", getStopTimeout().map(Object::toString).get()));
59
33
60 docker().stopContainer(cid.get(), options);
34 exec(docker().containerStop(getContainerId().get(), options));
61
62 if (getContainerIdFile().isPresent())
63 getContainerIdFile().getAsFile().get().delete();
64 }
35 }
65
36
66 }
37 }
@@ -28,7 +28,7 public abstract class TagImage extends D
28 }
28 }
29
29
30 public TagImage() {
30 public TagImage() {
31 this.setOnlyIf("No tags were specified", self -> getImageTags().size() > 0);
31 this.setOnlyIf(self -> getImageTags().size() > 0);
32 }
32 }
33
33
34 @TaskAction
34 @TaskAction
@@ -36,12 +36,9 public abstract class TagImage extends D
36 var tags = getImageTags();
36 var tags = getImageTags();
37 var src = getSrcImage().map(Object::toString).get();
37 var src = getSrcImage().map(Object::toString).get();
38
38
39 if (tags.size() == 0)
40 getLogger().info("No tags were specified");
41
42 for (var tag : tags) {
39 for (var tag : tags) {
43 getLogger().info("Tag: {}", tag);
40 getLogger().info("Tag: {}", tag);
44 docker().tagImage(src, tag);
41 exec(docker().imageTag(src, tag));
45 }
42 }
46 }
43 }
47 }
44 }
General Comments 0
You need to be logged in to leave comments. Login now