##// 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,17 +1,31
1 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 8 import org.gradle.api.provider.Property;
9 import org.gradle.api.provider.Provider;
4 10 import org.gradle.api.provider.SetProperty;
5 11
6 12 public abstract class ComposeExtension {
7 13 public final String COMPOSE_FILE = "compose.yaml";
8 14
9 15 public abstract SetProperty<String> getProfiles();
10 16
17 public abstract DirectoryProperty getContextDirectory();
18
11 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 28 getComposeFileName().convention(COMPOSE_FILE);
15 29 }
16 30
17 31 }
@@ -1,169 +1,158
1 1 package org.implab.gradle.containers;
2 2
3 3 import java.io.IOException;
4 4 import java.util.HashMap;
5 5 import java.util.Map;
6 6
7 7 import org.gradle.api.DefaultTask;
8 8 import org.gradle.api.Plugin;
9 9 import org.gradle.api.Project;
10 10 import org.gradle.api.artifacts.Configuration;
11 11 import org.gradle.api.artifacts.Configuration.State;
12 12 import org.gradle.api.logging.Logger;
13 13 import org.gradle.api.tasks.Copy;
14 14 import org.gradle.api.tasks.Delete;
15 15 import org.implab.gradle.containers.cli.Utils;
16 16 import org.implab.gradle.containers.tasks.ComposeRm;
17 17 import org.implab.gradle.containers.tasks.ComposeStop;
18 18 import org.implab.gradle.containers.tasks.ComposeUp;
19 19 import org.implab.gradle.containers.tasks.WriteEnv;
20 20
21 21 public abstract class ComposePlugin implements Plugin<Project>, ProjectMixin {
22 22 public static final String BUILD_GROUP = "build";
23 23
24 24 public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages";
25 25
26 26 public final String COMPOSE_PROJECT_NAME = "COMPOSE_PROJECT_NAME";
27 27
28 28 public final String COMPOSE_EXTENSION = "compose";
29 29
30 30 public final String COMPOSE_UP_TASK = "up";
31 31
32 32 public final String COMPOSE_STOP_TASK = "stop";
33 33
34 34 public final String COMPOSE_RM_TASK = "rm";
35 35
36 36 public final String CLEAN_TASK = "clean";
37 37
38 38 public final String BUILD_TASK = "build";
39 39
40 40 public final String PROCESS_RESOURCES_TASK = "processResources";
41 41
42 42 public final String WRITE_ENV_TASK = "writeEnv";
43 43
44 44 public final String COMPOSE_VAR = "composeVar";
45 45
46 46 public final String ENV_FILE_NAME = ".env";
47 47
48 48 public Logger getLogger() {
49 49 return getProject().getLogger();
50 50 }
51 51
52 52 @Override
53 53 public void apply(Project project) {
54 54 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
55 55
56 56 // basic configuration, register extension
57 var basePlugin = plugin(ContainerBasePlugin.class);
58 var containerExtension = basePlugin.getContainerExtension();
57 plugin(ContainerBasePlugin.class);
59 58
60 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 61 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
67 t.delete(containerExtension.getContextDirectory());
62 t.delete(composeExtension.getContextDirectory());
68 63 });
69 64
70 65 // copy task from src/main
71 66 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
72 67 t.mustRunAfter(cleanTask);
73 68 t.from(projectDirectory().dir("src/main"));
74 t.into(containerExtension.getContextDirectory());
69 t.into(composeExtension.getContextDirectory());
75 70 });
76 71
77 72 // write .env
78 73 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
79 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 77 var group = project.getGroup();
83 78 if (group != null && group.toString().length() > 0) {
84 79 t.getEnvironment().put(COMPOSE_PROJECT_NAME, group.toString());
85 80 }
86 81
87 82 t.getEnvironment().putAll(containerImages.map(this::extractComposeEnv));
88 83
89 84 });
90 85
91 86 var buildTask = task(BUILD_TASK, DefaultTask.class, t -> {
92 87 t.setGroup(BUILD_GROUP);
93 88 t.dependsOn(writeEnvTask);
94 89 });
95 90
96 91 var stopTask = task(COMPOSE_STOP_TASK, ComposeStop.class, t -> {
97 92 // stop must run after build
98 93 t.mustRunAfter(buildTask);
99 94
100 t.getProfiles().addAll(composeProfiles);
101 t.getComposeFile().set(composeFile);
102 95 });
103 96
104 97 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
105 98 // rm must run after build and stop
106 99 t.mustRunAfter(buildTask, stopTask);
107 100
108 t.getProfiles().addAll(composeProfiles);
109 t.getComposeFile().set(composeFile);
110 101 });
111 102
112 103 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
113 104 t.dependsOn(buildTask);
114 105 // up must run after stop and rm
115 106 t.mustRunAfter(stopTask, rmTask);
116 107
117 t.getProfiles().addAll(composeProfiles);
118 t.getComposeFile().set(composeFile);
119 108 });
120 109 }
121 110
122 111 /**
123 112 * Processed the configurations, extracts composeVar extra property from
124 113 * each dependency in this configuration and adds a value to the resulting
125 114 * map. The values in the nap will contain image tags.
126 115 */
127 116 private Map<String, String> extractComposeEnv(Configuration config) {
128 117 if (config.getState() != State.UNRESOLVED) {
129 118 getLogger().error("extractComposeEnv: The configuration {} isn't resolved.", config.getName());
130 119 throw new IllegalStateException("The specified configuration isn't resolved");
131 120 }
132 121
133 122 getLogger().info("extractComposeEnv {}", config.getName());
134 123
135 124 var map = new HashMap<String, String>();
136 125
137 126 for (var dependency : config.getDependencies()) {
138 127 // get extra composeVar if present
139 128 extra(dependency, COMPOSE_VAR, String.class).optional().ifPresent(varName -> {
140 129 // if we have a composeVar extra attribute on this dependency
141 130
142 131 // get files for the dependency
143 132 var files = config.files(dependency);
144 133 if (files.size() == 1) {
145 134 // should bw exactly 1 file
146 135 var file = files.stream().findAny().get();
147 136 getLogger().info("Processing {}: {}", dependency, file);
148 137
149 138 try {
150 139 // try read imageRef
151 140 Utils.readImageRef(file).getTag()
152 141 .ifPresentOrElse(
153 142 tag -> map.put(varName, tag),
154 143 () -> getLogger().error("ImageRef doesn't have a tag: {}", file));
155 144
156 145 } catch (IOException e) {
157 146 getLogger().error("Failed to read ImageRef {}: {}", file, e);
158 147 }
159 148
160 149 } else {
161 150 getLogger().warn("Dependency {} must have exactly 1 file", dependency);
162 151 }
163 152 });
164 153 }
165 154
166 155 return map;
167 156 }
168 157
169 158 }
@@ -1,56 +1,49
1 1 package org.implab.gradle.containers;
2 2
3 3 import org.gradle.api.Plugin;
4 4 import org.gradle.api.Project;
5 5 import org.gradle.api.plugins.ExtraPropertiesExtension;
6 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 12 import org.implab.gradle.containers.tasks.PushImage;
13 import org.implab.gradle.containers.tasks.RmContainer;
8 14 import org.implab.gradle.containers.tasks.RunContainer;
9 15 import org.implab.gradle.containers.tasks.SaveImage;
10 16 import org.implab.gradle.containers.tasks.StopContainer;
11 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 20 public static final String CONTAINER_EXTENSION_NAME = "container";
15 21
16 22 private ContainerExtension containerExtension;
17 23
18 24 ContainerExtension getContainerExtension() {
19 25 if (containerExtension == null)
20 26 throw new IllegalStateException();
21 27 return containerExtension;
22 28 }
23 29
24 30 void exportClasses(Project project, Class<?>... classes) {
25 31 ExtraPropertiesExtension extras = project.getExtensions().getExtraProperties();
26 32 for (var clazz : classes)
27 33 extras.set(clazz.getSimpleName(), clazz);
28 34 }
29 35
30 36 @Override
31 37 public void apply(Project project) {
32 38
33 containerExtension = project.getObjects().newInstance(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);
39 containerExtension = extension(CONTAINER_EXTENSION_NAME, ContainerExtension.class);
48 40
49 41 exportClasses(
50 42 project,
51 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
56 49 }
@@ -1,78 +1,88
1 1 package org.implab.gradle.containers;
2 2
3 3 import java.io.File;
4 4 import java.io.FileNotFoundException;
5 5 import java.io.IOException;
6 6 import java.util.Optional;
7 7
8 8 import javax.inject.Inject;
9 9
10 10 import org.gradle.api.Project;
11 11 import org.gradle.api.file.DirectoryProperty;
12 12 import org.gradle.api.file.ProjectLayout;
13 13 import org.gradle.api.provider.Property;
14 14 import org.gradle.api.provider.Provider;
15 15 import org.implab.gradle.containers.cli.ImageName;
16 16 import org.implab.gradle.containers.cli.ImageRef;
17 17 import org.implab.gradle.containers.cli.Utils;
18 18
19 19 public abstract class ContainerExtension implements PropertiesMixin {
20 20
21 21 public abstract Property<String> getCliCmd();
22 22
23 23 public abstract DirectoryProperty getContextDirectory();
24 24
25 25 /**
26 26 * Specifies the name of the registry where the image is located
27 27 * {@code registry.my-company.org}
28 28 */
29 29 public abstract Property<String> getImageAuthority();
30 30
31 31 /**
32 32 * Specified the path of the image like {@code my-company}
33 33 */
34 34 public abstract Property<String> getImageGroup();
35 35
36 36 public Provider<ImageName> getImageName() {
37 37 return provider(this::createImageName);
38 38 }
39 39
40 40 /**
41 41 * Specifies local image part like {@code httpd}
42 42 */
43 43 public abstract Property<String> getImageLocalName();
44 44
45 45 /**
46 46 * This property is deprecated use imageLocalName
47 47 */
48 48 @Deprecated
49 49 public Property<String> getImageShortName() {
50 50 return getImageLocalName();
51 51 }
52 52
53 53 public abstract Property<String> getImageTag();
54 54
55 55 @Inject
56 56 public ContainerExtension(ProjectLayout layout, Project project) {
57 57 getContextDirectory().convention(layout.getBuildDirectory().dir("context"));
58 58
59 59 getImageLocalName().convention(project.getName());
60 60
61 61 getImageTag().set(provider(() -> Optional.ofNullable(project.getVersion())
62 62 .map(Object::toString)
63 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 76 ImageName createImageName() {
67 77 return new ImageName(
68 78 getImageAuthority().getOrNull(),
69 79 getImageGroup().getOrNull(),
70 80 getImageLocalName().get(),
71 81 getImageTag().getOrNull());
72 82 }
73 83
74 84 public ImageRef readImageRef(File file) throws FileNotFoundException, IOException {
75 85 return Utils.readImageRef(file);
76 86 }
77 87
78 88 } No newline at end of file
@@ -1,80 +1,67
1 1 package org.implab.gradle.containers;
2 2
3 import org.gradle.api.GradleException;
3 import org.gradle.api.DefaultTask;
4 4 import org.gradle.api.Plugin;
5 5 import org.gradle.api.Project;
6 6 import org.gradle.api.artifacts.Dependency;
7 7 import org.gradle.api.file.ProjectLayout;
8 8 import org.gradle.api.tasks.Copy;
9 9 import org.gradle.api.tasks.Delete;
10 import org.gradle.api.tasks.TaskProvider;
11 10 import org.implab.gradle.containers.cli.ImageName;
12 11 import org.implab.gradle.containers.tasks.BuildImage;
13 12 import org.implab.gradle.containers.tasks.PushImage;
14 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 17 public static final String BUILD_GROUP = "build";
19 18
20 19 public static final String ARCHIVE_CONFIGURATION = "archive";
21 20
22 21 public void apply(Project project) {
23 22 ProjectLayout layout = project.getLayout();
24 23
25 project.getPluginManager().apply(ContainerBasePlugin.class);
26
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
24 var basePlugin = plugin(ContainerBasePlugin.class);
31 25 var containerExtension = basePlugin.getContainerExtension();
32 26
33 project.getConfigurations().create(Dependency.DEFAULT_CONFIGURATION, c -> {
34 c.setCanBeConsumed(true);
35 c.setCanBeResolved(false);
36 });
27 configuration(Dependency.DEFAULT_CONFIGURATION, Configurations.CONSUMABLE);
28 configuration(ARCHIVE_CONFIGURATION, Configurations.RESOLVABLE);
37 29
38 project.getConfigurations().create(ARCHIVE_CONFIGURATION, c -> {
39 c.setCanBeConsumed(true);
40 c.setCanBeResolved(false);
41 });
42
43 TaskProvider<Copy> processResourcesTask = project.getTasks().register("processResources", Copy.class, t -> {
30 var processResourcesTask = task("processResources", Copy.class, t -> {
44 31 t.setGroup(BUILD_GROUP);
45 32 t.from(layout.getProjectDirectory().dir("src/main"));
46 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 37 t.setGroup(BUILD_GROUP);
51 38 t.dependsOn(processResourcesTask);
52 39
53 40 t.getContextDirectory().set(containerExtension.getContextDirectory());
54 41 t.getImageIdFile().set(project.getLayout().getBuildDirectory().file("iid.json"));
55 42
56 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 47 t.delete(layout.getBuildDirectory());
61 48 });
62 49
63 project.getTasks().register("build", t -> {
50 task("build", DefaultTask.class, t -> {
64 51 t.setGroup(BUILD_GROUP);
65 52 t.dependsOn(buildImageTask);
66 53 });
67 54
68 project.getTasks().register("pushImage", PushImage.class, t -> {
55 task("pushImage", PushImage.class, t -> {
69 56 t.dependsOn(buildImageTask);
70 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 61 t.dependsOn(buildImageTask);
75 62 t.getExportImages().add(buildImageTask.flatMap(BuildImage::getImageName));
76 63 });
77 64
78 65 project.getArtifacts().add(Dependency.DEFAULT_CONFIGURATION, buildImageTask);
79 66 }
80 67 }
@@ -1,69 +1,69
1 1 package org.implab.gradle.containers;
2 2
3 3 import java.util.Collections;
4 4 import java.util.Optional;
5 5
6 6 import javax.inject.Inject;
7 7
8 8 import org.gradle.api.Action;
9 9 import org.gradle.api.NamedDomainObjectProvider;
10 10 import org.gradle.api.Plugin;
11 11 import org.gradle.api.Project;
12 12 import org.gradle.api.Task;
13 13 import org.gradle.api.artifacts.Configuration;
14 14 import org.gradle.api.file.Directory;
15 15 import org.gradle.api.tasks.TaskProvider;
16 16 import org.implab.gradle.containers.dsl.ExtraProps;
17 17 import org.implab.gradle.containers.dsl.MapEntry;
18 18
19 19 /** Project configuration traits */
20 20 public interface ProjectMixin {
21 21 @Inject
22 22 Project getProject();
23 23
24 24 /** registers the new task */
25 25 default <T extends Task> TaskProvider<T> task(String name, Class<T> clazz, Action<? super T> configure) {
26 26 return getProject().getTasks().register(name, clazz, configure);
27 27 }
28 28
29 29 /** Registers the new configuration */
30 30 default NamedDomainObjectProvider<Configuration> configuration(String name, Action<? super Configuration> configure) {
31 31 return getProject().getConfigurations().register(name, configure);
32 32 }
33 33
34 34 /** Returns the project directory */
35 35 default Directory projectDirectory() {
36 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 40 default <T extends Plugin<Project>> T plugin(Class<T> clazz) {
41 41 getProject().getPluginManager().apply(clazz);
42 42 return getProject().getPlugins().findPlugin(clazz);
43 43 }
44 44
45 45 /** Creates and register a new project extension.
46 46 *
47 47 * @param <T> The type of the extension
48 48 * @param extensionName The name of the extension in the project
49 49 * @param clazz The class of the extension
50 50 * @return the newly created extension
51 51 */
52 52 default <T> T extension(String extensionName, Class<T> clazz) {
53 53 T extension = getProject().getObjects().newInstance(clazz);
54 54 getProject().getExtensions().add(extensionName, extension);
55 55 return extension;
56 56 }
57 57
58 58 /** Return extra properties container for the specified object */
59 59 default Optional<ExtraProps> extra(Object target) {
60 60 return ExtraProps.extra(target);
61 61 }
62 62
63 63 /** Returns accessor for the specified extra property name */
64 64 default <T> MapEntry<T> extra(Object target, String prop, Class<T> clazz) {
65 65 return ExtraProps.extra(target)
66 66 .map(x -> x.prop(prop, clazz))
67 67 .orElseGet(() -> new MapEntry<T>(Collections.emptyMap(), prop, clazz));
68 68 }
69 69 }
@@ -1,162 +1,107
1 1 package org.implab.gradle.containers.cli;
2 2
3 3 import java.io.File;
4 4 import java.io.IOException;
5 5 import java.util.List;
6 import java.util.Optional;
7 6 import java.util.Set;
8 7 import java.util.concurrent.ExecutionException;
9 8
10 import org.gradle.api.logging.Logger;
11
12 9 public abstract class DockerTraits {
13 10
14 public final String BUILD_COMMAND = "build";
15 public final String PUSH_COMMAND = "push";
16 public final String RUN_COMMAND = "run";
17 public final String SAVE_COMMAND = "save";
18 public final String INSPECT_COMMAND = "inspect";
19 public final String IMAGE_COMMAND = "image";
20 public final String TAG_COMMAND = "tag";
21 public final String COMPOSE_COMMAND = "compose";
22 public final String UP_COMMAND = "up";
23 public final String STOP_COMMAND = "stop";
24 public final String RM_COMMAND = "rm";
25
26 public abstract Logger getLogger();
11 private final String BUILD_COMMAND = "build";
12 private final String PUSH_COMMAND = "push";
13 private final String RUN_COMMAND = "run";
14 private final String EXEC_COMMAND = "exec";
15 private final String SAVE_COMMAND = "save";
16 private final String INSPECT_COMMAND = "inspect";
17 private final String IMAGE_COMMAND = "image";
18 private final String TAG_COMMAND = "tag";
19 private final String COMPOSE_COMMAND = "compose";
20 private final String STOP_COMMAND = "stop";
21 private final String START_COMMAND = "start";
22 private final String CONTAINER_COMMAND = "container";
23 private final String RM_COMMAND = "rm";
27 24
28 public abstract Optional<File> getWorkingDir();
29
30 public abstract String getCliCmd();
25 protected abstract ProcessSpec builder(String... command);
31 26
32 protected boolean checkRetCode(ProcessSpec proc, int code)
33 throws InterruptedException, ExecutionException, IOException {
34 if (getLogger().isInfoEnabled()) {
35 proc.redirectStdout(RedirectTo.consumer(getLogger()::info))
36 .redirectStderr(RedirectTo.consumer(getLogger()::info));
37 }
38
39 getLogger().info("Starting: {}", proc.command());
40
41 return proc.start().get() == code;
27 public ProcessSpec imageBuild(String imageName, File contextDirectory, List<String> options) {
28 return builder(BUILD_COMMAND)
29 .args(options)
30 .args("-t", imageName, contextDirectory.getAbsolutePath());
42 31 }
43 32
44 protected void exec(ProcessSpec proc) throws InterruptedException, IOException, ExecutionException {
45 if (getLogger().isInfoEnabled())
46 proc.redirectStdout(RedirectTo.consumer(getLogger()::info));
47
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 }
33 public ProcessSpec imagePush(String image, List<String> options) {
34 return builder(PUSH_COMMAND)
35 .args(options)
36 .args(image);
58 37 }
59 38
60 protected ProcessSpec builder(String... args) {
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 {
39 public ProcessSpec containerRun(String image, List<String> options, List<String> command) {
88 40 return builder(RUN_COMMAND)
89 41 .args(options)
90 42 .args(image)
91 43 .args(command);
92 44 }
93 45
94 public void stopContainer(String containerId, List<String> options) throws InterruptedException, IOException, ExecutionException {
95 exec(builder(STOP_COMMAND, containerId).args(options));
46 public ProcessSpec containerExec(String containerId, List<String> options, List<String> command) {
47 return builder(EXEC_COMMAND)
48 .args(options)
49 .args(containerId)
50 .args(command);
51 }
52
53 public ProcessSpec containerStop(String containerId, List<String> options) {
54 return builder(STOP_COMMAND, containerId).args(options);
55 }
56
57 public ProcessSpec containerStart(Set<String> containers, List<String> options) {
58 return builder(START_COMMAND).args(options).args(containers);
96 59 }
97 60
98 public void saveImage(Set<String> images, File output)
99 throws InterruptedException, IOException, ExecutionException {
61 public ProcessSpec containerRm(Set<String> containers, boolean removeVolumes, boolean force) {
62 var spec = builder(RM_COMMAND);
63
64 if(removeVolumes)
65 spec.args("--volumes");
66 if (force)
67 spec.args("--force");
68
69 spec.args(containers);
70
71 return spec;
72 }
73
74 public ProcessSpec imageSave(Set<String> images, File output) {
100 75 if (output.exists())
101 76 output.delete();
102 77
103 var spec = builder(SAVE_COMMAND)
78 return builder(SAVE_COMMAND)
104 79 .args("-o", output.getAbsolutePath())
105 80 .args(images);
106
107 exec(spec);
108 81 }
109 82
110 public void tagImage(String source, String target) throws InterruptedException, IOException, ExecutionException {
111 exec(builder(TAG_COMMAND, source, target));
83 public ProcessSpec imageTag(String source, String target) {
84 return builder(TAG_COMMAND, source, target);
112 85 }
113 86
114 public boolean imageExists(String imageId) throws InterruptedException, IOException, ExecutionException {
115 getLogger().info("Check image {} exists", imageId);
116
117 return checkRetCode(
118 builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId),
119 0);
87 public ProcessSpec imageExists(String imageId) throws InterruptedException, IOException, ExecutionException {
88 return builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId);
120 89 }
121 90
122 public boolean imageExists(File imageIdFile) {
123 if (imageIdFile.exists()) {
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;
91 public ProcessSpec containerExists(String imageId) throws InterruptedException, IOException, ExecutionException {
92 return builder(CONTAINER_COMMAND, INSPECT_COMMAND, "--format", "container-exists", imageId);
133 93 }
134 94
135 ProcessSpec compose(File primaryCompose, Set<String> profiles, String... extra) {
95 public ComposeTraits compose(File primaryCompose, Set<String> profiles) {
96 return new ComposeTraits() {
97 @Override
98 public ProcessSpec compose(String command) {
136 99 var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath());
137 100
138 101 for (var profile : profiles)
139 102 spec.args("--profile", profile);
140
141 spec.args(extra);
142
143 return spec;
144 }
145
146 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
147 exec(compose(primaryCompose, profiles, UP_COMMAND, "--detach"));
103 return spec.args(command);
148 104 }
149
150 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
151 exec(compose(primaryCompose, profiles, STOP_COMMAND));
152 }
153
154 public void composeRm(File primaryCompose, Set<String> profiles, boolean removeVolumes)
155 throws InterruptedException, IOException, ExecutionException {
156 var spec = compose(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
157 if (removeVolumes)
158 spec.args("--volumes");
159
160 exec(spec);
105 };
161 106 }
162 107 }
@@ -1,32 +1,31
1 1 package org.implab.gradle.containers.cli;
2 2
3 3 import java.io.File;
4 4 import java.io.FileInputStream;
5 5 import java.io.InputStream;
6 6 import java.io.OutputStream;
7 import java.util.concurrent.Callable;
8 7 import java.util.concurrent.CompletableFuture;
9 8
10 9 public interface RedirectFrom {
11 10 CompletableFuture<Void> redirect(OutputStream to);
12 11
13 12 public static RedirectFrom file(final File file) {
14 13 return to -> CompletableFuture.runAsync(() -> {
15 14 try (var from = new FileInputStream(file); to) {
16 15 from.transferTo(to);
17 16 } catch (Exception e) {
18 17 // silence!
19 18 }
20 19 });
21 20 }
22 21
23 22 public static RedirectFrom stream(final InputStream from) {
24 23 return to -> CompletableFuture.runAsync(() -> {
25 24 try (from; to) {
26 25 from.transferTo(to);
27 26 } catch (Exception e) {
28 27 // silence!
29 28 }
30 29 });
31 30 }
32 31 }
@@ -1,156 +1,170
1 1 package org.implab.gradle.containers.cli;
2 2
3 3 import java.io.ByteArrayOutputStream;
4 4 import java.io.Closeable;
5 5 import java.io.File;
6 6 import java.io.FileNotFoundException;
7 7 import java.io.FileOutputStream;
8 8 import java.io.FileReader;
9 9 import java.io.IOException;
10 10 import java.io.InputStream;
11 11 import java.io.OutputStream;
12 12 import java.io.Reader;
13 13 import java.util.Scanner;
14 14 import java.util.Set;
15 15 import java.util.concurrent.CompletableFuture;
16 16 import java.util.stream.Collectors;
17 17 import java.util.stream.StreamSupport;
18 18 import java.nio.file.Files;
19 19 import java.util.List;
20 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 24 import org.gradle.internal.impldep.org.bouncycastle.util.Iterable;
22 25
23 26 import com.fasterxml.jackson.core.exc.StreamWriteException;
24 27 import com.fasterxml.jackson.databind.DatabindException;
25 28 import com.fasterxml.jackson.databind.ObjectMapper;
26 29 import com.fasterxml.jackson.databind.SerializationFeature;
27 30 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
28 31
29 32 import groovy.json.JsonGenerator;
30 33 import groovy.json.JsonOutput;
31 34 import groovy.json.JsonGenerator.Converter;
32 35 import groovy.lang.Closure;
33 36
34 37 public final class Utils {
35 38 public static CompletableFuture<Void> redirectIO(final InputStream src, final Action<String> consumer) {
36 39 return CompletableFuture.runAsync(() -> {
37 40 try (Scanner sc = new Scanner(src)) {
38 41 while (sc.hasNextLine()) {
39 42 consumer.execute(sc.nextLine());
40 43 }
41 44 }
42 45 });
43 46 }
44 47
45 48 public static CompletableFuture<Void> redirectIO(final InputStream src, final File file) {
46 49 return CompletableFuture.runAsync(() -> {
47 50 try (OutputStream out = new FileOutputStream(file)) {
48 51 src.transferTo(out);
49 52 } catch (Exception e) {
50 53 // silence!
51 54 }
52 55 });
53 56 }
54 57
55 58 public static CompletableFuture<Void> redirectIO(final InputStream src, final OutputStream dst) {
56 59 return CompletableFuture.runAsync(() -> {
57 60 try (dst) {
58 61 src.transferTo(dst);
59 62 } catch (Exception e) {
60 63 // silence!
61 64 }
62 65 });
63 66 }
64 67
65 68 public static void closeSilent(Closeable handle) {
66 69 try {
67 70 handle.close();
68 71 } catch (Exception e) {
69 72 // silence!
70 73 }
71 74 }
72 75
73 76 public static String readAll(final InputStream src) throws IOException {
74 77 ByteArrayOutputStream out = new ByteArrayOutputStream();
75 78 src.transferTo(out);
76 79 return out.toString();
77 80 }
78 81
79 82 public static <T> T readJson(final Reader reader, Class<T> type) throws IOException {
80 83 ObjectMapper objectMapper = new ObjectMapper()
81 84 .registerModule(new Jdk8Module());
82 85 return objectMapper.readValue(reader, type);
83 86 }
84 87
85 88 public static void writeJson(final File file, Object value)
86 89 throws StreamWriteException, DatabindException, IOException {
87 90 ObjectMapper objectMapper = new ObjectMapper()
88 91 .enable(SerializationFeature.INDENT_OUTPUT)
89 92 .registerModule(new Jdk8Module());
90 93 objectMapper.writeValue(file, value);
91 94 }
92 95
93 96 public static ImageRef readImageRef(final File file) throws FileNotFoundException, IOException {
94 97 try (var reader = new FileReader(file)) {
95 98 return readJson(reader, ImageRef.class);
96 99 }
97 100 }
98 101
99 public static String readAll(final File src) throws IOException {
100 return Files.readString(src.toPath());
102 public static String readAll(final File src) {
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 110 public static List<String> readAll(final Iterable<? extends File> files) {
104 111 return StreamSupport.stream(files.spliterator(), false)
105 .map(file -> {
106 try {
107 return Utils.readAll(file);
108 } catch (IOException e) {
109 throw new RuntimeException(e);
110 }
111 })
112 .map(Utils::readAll)
112 113 .toList();
113 114 }
114 115
115 116 public static String readAll(final InputStream src, String charset) throws IOException {
116 117 ByteArrayOutputStream out = new ByteArrayOutputStream();
117 118 src.transferTo(out);
118 119 return out.toString(charset);
119 120 }
120 121
121 122 public static JsonGenerator createDefaultJsonGenerator() {
122 123 return new JsonGenerator.Options()
123 124 .excludeNulls()
124 125 .addConverter(new Converter() {
125 126 public boolean handles(Class<?> type) {
126 127 return (File.class == type);
127 128 }
128 129
129 130 public Object convert(Object value, String key) {
130 131 return ((File) value).getPath();
131 132 }
132 133 })
133 134 .build();
134 135 }
135 136
136 137 public static String toJsonPretty(Object value) {
137 138 return JsonOutput.prettyPrint(createDefaultJsonGenerator().toJson(value));
138 139 }
139 140
140 141 public static boolean isNullOrEmptyString(String value) {
141 142 return (value == null || value.length() == 0);
142 143 }
143 144
144 145 public static <T> Action<T> wrapClosure(Closure<?> closure) {
145 146 return x -> {
146 147 closure.setDelegate(x);
147 148 closure.setResolveStrategy(Closure.OWNER_FIRST);
148 149 closure.call(x);
149 150 };
150 151 }
151 152
152 153 public static Set<String> mapToString(Set<Object> set) {
153 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 170 } No newline at end of file
@@ -1,39 +1,41
1 1 package org.implab.gradle.containers.dsl;
2 2
3 3 import java.util.concurrent.Callable;
4 4
5 5 import org.gradle.api.provider.ListProperty;
6 6 import org.gradle.api.tasks.Input;
7 import org.gradle.api.tasks.Optional;
7 8 import org.implab.gradle.containers.PropertiesMixin;
8 9
9 10 import groovy.lang.Closure;
10 11
11 12 public interface OptionsMixin extends PropertiesMixin {
12 13
13 14 @Input
15 @Optional
14 16 ListProperty<String> getOptions();
15 17
16 18 default void option(String opt) {
17 19 getOptions().add(opt);
18 20 }
19 21
20 22 default void option(Callable<String> optionProvider) {
21 23 getOptions().add(provider(optionProvider));
22 24 }
23 25
24 26 default void option(Closure<String> optionProvider) {
25 27 getOptions().add(provider(optionProvider));
26 28 }
27 29
28 30 default void options(String ...opts) {
29 31 getOptions().addAll(opts);
30 32 }
31 33
32 34 default void options(Callable<Iterable<String>> optionsProvider) {
33 35 getOptions().addAll(provider(optionsProvider));
34 36 }
35 37
36 38 default void options(Closure<Iterable<String>> optionsProvider) {
37 39 getOptions().addAll(provider(optionsProvider));
38 40 }
39 41 }
@@ -1,34 +1,34
1 1 package org.implab.gradle.containers.dsl;
2 2
3 3 import java.util.ArrayList;
4 4
5 import org.gradle.api.provider.ListProperty;
6 5 import org.gradle.api.provider.Property;
6 import org.gradle.api.provider.SetProperty;
7 7
8 8 public abstract class VolumeSpec {
9 9
10 10 public abstract Property<String> getSource();
11 11
12 12 public abstract Property<String> getTarget();
13 13
14 public abstract ListProperty<String> getOptions();
14 public abstract SetProperty<String> getOptions();
15 15
16 16 public void ro() {
17 17 getOptions().add("ro");
18 18 }
19 19
20 20 public String resolveSpec() {
21 21 var parts = new ArrayList<String>();
22 22
23 23 if (getSource().isPresent())
24 24 parts.add(getSource().get());
25 25
26 26 parts.add(getTarget().get());
27 27
28 28 if (getOptions().isPresent())
29 29 parts.add(String.join(",", getOptions().get()));
30 30
31 31 return String.join(":", parts);
32 32 }
33 33
34 34 }
@@ -1,116 +1,132
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.util.ArrayList;
4 4 import java.util.HashMap;
5 5 import java.util.List;
6 6 import java.util.Map;
7 7
8 8 import org.gradle.api.Action;
9 9 import org.gradle.api.file.Directory;
10 10 import org.gradle.api.file.DirectoryProperty;
11 11 import org.gradle.api.file.RegularFile;
12 12 import org.gradle.api.file.RegularFileProperty;
13 13 import org.gradle.api.provider.MapProperty;
14 14 import org.gradle.api.provider.Property;
15 15 import org.gradle.api.provider.Provider;
16 16 import org.gradle.api.tasks.Input;
17 17 import org.gradle.api.tasks.InputDirectory;
18 18 import org.gradle.api.tasks.Internal;
19 19 import org.gradle.api.tasks.Optional;
20 20 import org.gradle.api.tasks.OutputFile;
21 21 import org.gradle.api.tasks.SkipWhenEmpty;
22 22 import org.gradle.api.tasks.TaskAction;
23 23 import java.io.File;
24 import java.io.IOException;
24 25
25 26 import org.implab.gradle.containers.cli.ImageRef;
26 27 import org.implab.gradle.containers.cli.Utils;
27 28 import org.implab.gradle.containers.dsl.MapPropertyEntry;
28 29 import org.implab.gradle.containers.dsl.OptionsMixin;
29 30
30 31 import groovy.lang.Closure;
31 32
32 33 public abstract class BuildImage extends DockerCliTask implements OptionsMixin {
33 34
34 35 @InputDirectory
35 36 @SkipWhenEmpty
36 37 public abstract DirectoryProperty getContextDirectory();
37 38
38 39 @Input
39 40 public abstract MapProperty<String, String> getBuildArgs();
40 41
41 42 @Input
42 43 @Optional
43 44 public abstract Property<String> getBuildTarget();
44 45
45 46 @Input
46 47 public abstract Property<Object> getImageName();
47 48
48 49 @Internal
49 50 public abstract RegularFileProperty getImageIdFile();
50 51
51 52 @OutputFile
52 53 public Provider<File> getImageIdFileOutput() {
53 54 return getImageIdFile().map(RegularFile::getAsFile);
54 55 }
55 56
56 57 public BuildImage() {
57 58 getOutputs().upToDateWhen(task -> getImageIdFile()
58 .map(RegularFile::getAsFile)
59 .map(docker()::imageExists)
59 .map(this::imageExists)
60 60 .getOrElse(false));
61 61 }
62 62
63 63 public void buildArgs(Action<Map<String, String>> spec) {
64 64 getBuildArgs().putAll(provider(() -> {
65 65 Map<String, String> args = new HashMap<>();
66 66 spec.execute(args);
67 67 getLogger().info("add buildArgs {}", args);
68 68 return args;
69 69 }));
70 70 }
71 71
72 72 public void buildArgs(Closure<Map<String, String>> spec) {
73 73 buildArgs(Utils.wrapClosure(spec));
74 74 }
75 75
76 76 public MapPropertyEntry<String, String> buildArg(String key) {
77 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 94 @TaskAction
81 95 public void run() throws Exception {
82 96 List<String> args = new ArrayList<>();
83 97
84 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 101 // specify where to write image id
88 102 args.addAll(List.of("--iidfile", iidFile.getAbsolutePath()));
89 103
90 104 getBuildArgs().get().forEach((k, v) -> {
91 105 args.add("--build-arg");
92 106 args.add(String.format("%s=%s", k, v));
93 107 });
94 108
95 109 // add --target if specified for multi-stage build
96 110 if (getBuildTarget().isPresent()) {
97 111 args.add("--target");
98 112 args.add(getBuildTarget().get());
99 113 }
100 114
101 115 // add extra parameters
102 116 getOptions().get().forEach(args::add);
103 117
104 118 var imageTag = getImageName().map(Object::toString).get();
105 119
106 120 // build image
107 docker().buildImage(
121 var spec = docker().imageBuild(
108 122 imageTag,
109 123 getContextDirectory().map(Directory::getAsFile).get(),
110 124 args);
111 125
126 exec(spec);
127
112 128 // read the built image id and store image ref metadata
113 129 var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile));
114 130 Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef);
115 131 }
116 132 }
@@ -1,10 +1,31
1 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 7 import org.gradle.api.provider.Property;
4 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 13 @Internal
9 14 public abstract Property<String> getServiceName();
15
16 public ComposeExec() {
17 applyComposeConvention();
10 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 }
31 }
@@ -1,23 +1,24
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.concurrent.ExecutionException;
5 5
6 6 import org.gradle.api.provider.Property;
7 7 import org.gradle.api.tasks.Internal;
8 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 12 @Internal
13 13 public abstract Property<Boolean> getRemoveVolumes();
14 14
15 15 public ComposeRm() {
16 applyComposeConvention();
16 17 getRemoveVolumes().convention(true);
17 18 }
18 19
19 20 @TaskAction
20 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 }
@@ -1,14 +1,18
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.concurrent.ExecutionException;
5 5
6 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 14 @TaskAction
11 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 18 } No newline at end of file
@@ -1,33 +1,37
1 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 7 import org.gradle.api.file.RegularFileProperty;
4 8 import org.gradle.api.provider.SetProperty;
5 9 import org.gradle.api.tasks.Internal;
10 import org.implab.gradle.containers.ComposeExtension;
6 11
7 12 /**
8 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 17 /** The list of profiles to use with the compose commands */
13 18 @Internal
14 public abstract SetProperty<String> getProfiles();
19 SetProperty<String> getProfiles();
15 20
16 21 /**
17 22 * The primary compose files. This task is executed regardless whether these
18 23 * files was changed.
19 24 */
20 25 @Internal
21 public abstract RegularFileProperty getComposeFile();
26 RegularFileProperty getComposeFile();
22 27
23 protected ComposeTask() {
24 // the task can only be evaluated if the compose file is present
25 setOnlyIf(self -> {
26 var composeFile = getComposeFile().get().getAsFile();
27 var exists = composeFile.exists();
28 getLogger().info("file: {} {}", composeFile.toString(), exists ? "exists" : "doesn't exist");
29 return exists;
28 default void applyComposeConvention() {
29 onlyIf(task -> getComposeFile().map(RegularFile::getAsFile).map(File::exists).getOrElse(false));
30 Optional.ofNullable(getProject().getExtensions().findByType(ComposeExtension.class))
31 .ifPresent(ext -> {
32 getProfiles().set(ext.getProfiles());
33 getComposeFile().set(ext.getComposeFile());
30 34 });
31 }
32 35
33 36 }
37 }
@@ -1,14 +1,31
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.concurrent.ExecutionException;
5 5
6 import org.gradle.api.tasks.Internal;
6 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 26 @TaskAction
11 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,89 +1,150
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.File;
4 import java.io.IOException;
4 5 import java.util.Optional;
6 import java.util.concurrent.ExecutionException;
5 7
6 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 11 import org.gradle.api.provider.Property;
9 12 import org.gradle.api.tasks.Input;
10 13 import org.gradle.api.tasks.Internal;
11 14 import org.implab.gradle.containers.ContainerExtension;
12 15 import org.implab.gradle.containers.PropertiesMixin;
16 import org.implab.gradle.containers.cli.ComposeTraits;
13 17 import org.implab.gradle.containers.cli.DockerTraits;
14 18 import org.implab.gradle.containers.cli.ProcessSpec;
15 19 import org.implab.gradle.containers.cli.RedirectFrom;
16 20 import org.implab.gradle.containers.cli.RedirectTo;
21 import org.implab.gradle.containers.cli.Utils;
17 22
18 23 public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin {
19 24
20 25 @Input
21 26 public abstract Property<String> getCliCmd();
22 27
23 28 /**
24 29 * Returns working directory for docker commands
25 30 */
26 31 @Input
27 32 @org.gradle.api.tasks.Optional
28 33 public abstract Property<File> getWorkingDirectory();
29 34
30 35 @Internal
31 36 protected ContainerExtension getContainerExtension() {
32 37 return getProject().getExtensions().getByType(ContainerExtension.class);
33 38 }
34 39
35 40 public DockerCliTask() {
36 41 getCliCmd().convention(getContainerExtension().getCliCmd());
37 42
38 43 }
39 44
40 45 protected Optional<RedirectTo> loggerInfoRedirect() {
41 46 return getLogger().isInfoEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::info)) : Optional.empty();
42 47 }
43 48
44 49 protected Optional<RedirectTo> loggerErrorRedirect() {
45 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 54 return loggerInfoRedirect();
50 55 }
51 56
52 protected Optional<RedirectTo> getStderrRedirection() {
57 protected Optional<RedirectTo> stderrRedirection() {
53 58 return loggerErrorRedirect();
54 59 }
55 60
56 protected Optional<RedirectFrom> getStdinRedirection() {
61 protected Optional<RedirectFrom> stdinRedirection() {
57 62 return Optional.empty();
58 63 }
59 64
60 protected DockerTraits docker() {
65 protected TaskDockerTraits docker() {
61 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 138 class TaskDockerTraits extends DockerTraits {
70 139
71 140 @Override
72 public Logger getLogger() {
73 return DockerCliTask.this.getLogger();
141 protected ProcessSpec builder(String... command) {
142 return commandBuilder(command);
74 143 }
75 144
76 @Override
77 public Optional<File> getWorkingDir() {
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();
145 public ComposeTraits compose(ComposeTaskMixin props) {
146 return compose(props.getComposeFile().map(RegularFile::getAsFile).get(), props.getProfiles().get());
86 147 }
87 148
88 149 }
89 150 }
@@ -1,33 +1,28
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.concurrent.ExecutionException;
5 5
6 import org.gradle.api.provider.ListProperty;
7 6 import org.gradle.api.provider.Property;
8 7 import org.gradle.api.tasks.Input;
9 8 import org.gradle.api.tasks.Optional;
10 9 import org.gradle.api.tasks.TaskAction;
11 10 import org.implab.gradle.containers.dsl.OptionsMixin;
12 11
13 12 public abstract class PushImage extends DockerCliTask implements OptionsMixin {
14 13
15 14 @Input
16 15 public abstract Property<Object> getImageName();
17 16
18 17 @Input
19 18 @Optional
20 19 public abstract Property<Object> getTransport();
21 20
22 @Input
23 @Optional
24 public abstract ListProperty<String> getOptions();
25
26 21 @TaskAction
27 22 public void run() throws InterruptedException, IOException, ExecutionException {
28 23
29 docker().pushImage(
24 exec(docker().imagePush(
30 25 getImageName().map(Object::toString).get(),
31 getOptions().get());
26 getOptions().get()));
32 27 }
33 28 }
@@ -1,159 +1,125
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.File;
4 4 import java.io.IOException;
5 5 import java.util.ArrayList;
6 6 import java.util.List;
7 import java.util.Optional;
7 import java.util.Set;
8 8 import java.util.concurrent.ExecutionException;
9 9
10 10 import org.gradle.api.Action;
11 import org.gradle.api.GradleException;
11 12 import org.gradle.api.file.RegularFileProperty;
12 import org.gradle.api.provider.ListProperty;
13 13 import org.gradle.api.provider.Property;
14 14 import org.gradle.api.tasks.Internal;
15 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 16 import org.implab.gradle.containers.dsl.VolumeSpec;
22 17
23 public abstract class RunContainer extends DockerCliTask implements OptionsMixin {
24
25 private final RedirectToSpec redirectStderr = new RedirectToSpec();
26
27 private final RedirectToSpec redirectStdout = new RedirectToSpec();
28
29 private final RedirectFromSpec redirectStdin = new RedirectFromSpec();
18 public abstract class RunContainer extends BaseExecTask implements ContainerTaskMixin {
30 19
31 20 private boolean transientContainer = true;
32 21
33 private boolean detached = false;
34
35 private boolean interactive = false;
22 private boolean startIfExists = true;
36 23
37 24 @Internal
38 25 public abstract Property<Object> getImageName();
39 26
40 27 @Internal
41 public abstract ListProperty<String> getCommandLine();
42
43 @Internal
44 public RedirectFromSpec getStdin() {
45 return redirectStdin;
46 }
28 public abstract Property<String> getContainerName();
47 29
48 @Internal
49 public RedirectToSpec getStdout() {
50 return redirectStdout;
51 }
52
53 @Internal
54 public RedirectToSpec getStderr() {
55 return redirectStderr;
56 }
57
30 /**
31 * Transient containers will be removed after completion. Default is `true`
32 */
58 33 @Internal
59 34 public boolean isTransientContainer() {
60 35 return transientContainer;
61 36 }
62 37
63 38 public void setTransientContainer(boolean value) {
64 39 transientContainer = value;
65 40 }
66 41
67 42 @Internal
68 public boolean isDetached() {
69 // if IO was redirected the container should run in foreground
70 if (redirectStdin.isRedirected() || redirectStdout.isRedirected() || redirectStderr.isRedirected())
71 return false;
43 public boolean getStartIfExists() {
44 return startIfExists;
45 }
72 46
73 return detached;
47 public void setStartIdExists(boolean value) {
48 startIfExists = value;
74 49 }
75 50
76 public void setDetached(boolean value) {
77 detached = value;
78 }
79
80 @Internal
81 public boolean isInteractive() {
82 // enable interactive mode when processing standard input
83 return redirectStdin.isRedirected() || interactive;
84 }
85
51 /**
52 * Specified the file where the stared container id will be written.
53 * Default value is a temporary file.
54 *
55 * <p>
56 * This property can be use in tasks where the container id is required
57 * to perform operation, for example to StopContainer tasks.
58 */
86 59 @Internal
87 60 public abstract RegularFileProperty getContainerIdFile();
88 61
89 62 public RunContainer() {
90 63 getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid"));
91 64 }
92 65
66 /**
67 * Adds volume specification
68 *
69 * @param configure
70 */
93 71 public void volume(Action<VolumeSpec> configure) {
94 72 getOptions().add("-v");
95 73 getOptions().add(provider(() -> {
96 74 var volumeSpec = getObjectFactory().newInstance(VolumeSpec.class);
97 75 configure.execute(volumeSpec);
98 76 return volumeSpec.resolveSpec();
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 80 @TaskAction
122 81 public void run() throws InterruptedException, IOException, ExecutionException {
123 82 var params = new ArrayList<>(getOptions().get());
124 83
84 if (getContainerId().map(this::containerExists).getOrElse(false)) {
85 var cid = getContainerId().get();
86 if (getStartIfExists()) {
87 getLogger().info("Container {} already created, starting.", cid);
88
89 if (isInteractive())
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 {
99
125 100 if (isInteractive())
126 101 params.add("--interactive");
127 102 if (isTransientContainer())
128 103 params.add("--rm");
129 104 if (isDetached())
130 105 params.add("--detach");
106 if (getAllocateTty())
107 params.add("--tty");
108 if (getContainerName().isPresent())
109 params.addAll(List.of("--name", getContainerName().get()));
110
131 111 if (getContainerIdFile().isPresent()) {
112 // delete previous container id otherwise the container will fail to start
132 113 getContainerIdFile().getAsFile().get().delete();
133 114
134 115 params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString()));
135 116 }
136 117
137 var spec = docker().runImage(
118 exec(docker().containerRun(
138 119 getImageName().map(Object::toString).get(),
139 120 params,
140 getCommandLine().get());
141
142 // if process will run in foreground, then setup default redirects
143 redirectStdout.getRedirection()
144 .or(this::loggerInfoRedirect)
145 .ifPresent(spec::redirectStdout);
146
147 redirectStderr.getRedirection()
148 .or(this::loggerErrorRedirect)
149 .ifPresent(spec::redirectStderr);
150
151 redirectStdin.getRedirection().ifPresent(spec::redirectStdin);
152
153 getLogger().info("Staring: {}", spec.command());
154
155 // runs the command and checks the error code
156 spec.exec();
121 getCommandLine().get()));
122 }
157 123 }
158 124
159 125 }
@@ -1,112 +1,112
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.File;
4 4 import java.io.IOException;
5 5 import java.util.ArrayList;
6 6 import java.util.Optional;
7 7 import java.util.concurrent.ExecutionException;
8 8
9 9 import org.gradle.api.file.DirectoryProperty;
10 10 import org.gradle.api.file.FileCollection;
11 11 import org.gradle.api.file.RegularFile;
12 12 import org.gradle.api.provider.Property;
13 13 import org.gradle.api.provider.Provider;
14 14 import org.gradle.api.provider.SetProperty;
15 15 import org.gradle.api.tasks.Input;
16 16 import org.gradle.api.tasks.Internal;
17 17 import org.gradle.api.tasks.OutputFile;
18 18 import org.gradle.api.tasks.TaskAction;
19 19 import org.gradle.api.tasks.TaskExecutionException;
20 20 import org.implab.gradle.containers.cli.Utils;
21 21
22 22 public abstract class SaveImage extends DockerCliTask {
23 23
24 24 @Input
25 25 public abstract SetProperty<Object> getExportImages();
26 26
27 27 @OutputFile
28 28 public Provider<RegularFile> getArchiveFile() {
29 29 return getDestinationDirectory().file(getArchiveFileName());
30 30 }
31 31
32 32 @Internal
33 33 public abstract DirectoryProperty getDestinationDirectory();
34 34
35 35 @Internal
36 36 public abstract Property<String> getArchiveFileName();
37 37
38 38 @Internal
39 39 public abstract Property<String> getArchiveBaseName();
40 40
41 41 @Internal
42 42 public abstract Property<String> getArchiveVersion();
43 43
44 44 @Internal
45 45 public abstract Property<String> getArchiveClassifier();
46 46
47 47 @Internal
48 48 public abstract Property<String> getArchiveExtension();
49 49
50 50 String readImageRefTag(File file) throws Exception {
51 51 getLogger().info("Reading image ref from {}", file);
52 52 var imageRef = Utils.readImageRef(file);
53 53 return imageRef.getTag().orElseThrow(() -> new Exception("The image tag is required to save image"));
54 54 }
55 55
56 56 public void imageRef(File file) {
57 57 getExportImages().add(provider(() -> {
58 58 return readImageRefTag(file);
59 59 }));
60 60 }
61 61
62 62 public void imageRefs(FileCollection files) {
63 63 dependsOn(files);
64 64 getExportImages().addAll(provider(() -> {
65 65 var tags = new ArrayList<String>();
66 66 for (File file : files) {
67 67 tags.add(readImageRefTag(file));
68 68 }
69 69 return tags;
70 70 }));
71 71 }
72 72
73 73 public SaveImage() {
74 74 getArchiveFileName().convention(provider(this::conventionalArchiveFileName));
75 75 getArchiveBaseName().convention(provider(this::conventionalArchiveBaseName));
76 76 getArchiveVersion().convention(provider(() -> getProject().getVersion()).map(x -> x.toString()));
77 77 getDestinationDirectory().convention(getProject().getLayout().getBuildDirectory());
78 78 getArchiveExtension().convention("tar");
79 79 }
80 80
81 81 private String conventionalArchiveBaseName() {
82 82 ArrayList<String> parts = new ArrayList<>();
83 83 Optional.of(getProject().getGroup()).map(x -> x.toString()).ifPresent(parts::add);
84 84 parts.add(getProject().getName());
85 85 return String.join("-", parts);
86 86 }
87 87
88 88 private String conventionalArchiveFileName() {
89 89 ArrayList<String> parts = new ArrayList<>();
90 90
91 91 if (getArchiveBaseName().isPresent())
92 92 parts.add(getArchiveBaseName().get());
93 93
94 94 if (getArchiveVersion().isPresent())
95 95 parts.add(getArchiveVersion().get());
96 96
97 97 if (getArchiveClassifier().isPresent())
98 98 parts.add(getArchiveClassifier().get());
99 99
100 100 if (parts.size() == 0)
101 101 throw new TaskExecutionException(this, new Exception("The archiveFileName ism't specified"));
102 102
103 103 return String.join("-", parts) + "." + getArchiveExtension().get();
104 104 }
105 105
106 106 @TaskAction
107 107 public void run() throws InterruptedException, IOException, ExecutionException {
108 docker().saveImage(
108 exec(docker().imageSave(
109 109 getExportImages().map(Utils::mapToString).get(),
110 getArchiveFile().map(RegularFile::getAsFile).get());
110 getArchiveFile().map(RegularFile::getAsFile).get()));
111 111 }
112 112 }
@@ -1,66 +1,37
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.ArrayList;
5 5 import java.util.List;
6 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 8 import org.gradle.api.provider.Property;
12 import org.gradle.api.provider.Provider;
13 9 import org.gradle.api.tasks.Internal;
14 10 import org.gradle.api.tasks.TaskAction;
15 import org.implab.gradle.containers.cli.Utils;
16 11
17 public abstract class StopContainer extends DockerCliTask {
18
19 @Internal
20 public abstract RegularFileProperty getContainerIdFile();
21
22 @Internal
23 public abstract Property<String> getContainerName();
12 public abstract class StopContainer extends DockerCliTask implements ContainerTaskMixin {
24 13
25 14 @Internal
26 15 public abstract Property<String> getStopSignal();
27 16
28 17 @Internal
29 18 public abstract Property<Integer> getStopTimeout();
30 19
31 Provider<String> readCid(RegularFile regularFile) {
32 var file = regularFile.getAsFile();
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 });
20 public StopContainer() {
21 onlyIfReason("Container doesn't exists", self -> getContainerId().map(this::containerExists).getOrElse(false));
41 22 }
42 23
43 24 @TaskAction
44 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 26 var options = new ArrayList<String>();
53 27
54 28 if (getStopSignal().isPresent())
55 29 options.addAll(List.of("--signal", getStopSignal().get()));
56 30
57 31 if (getStopTimeout().isPresent())
58 32 options.addAll(List.of("--time", getStopTimeout().map(Object::toString).get()));
59 33
60 docker().stopContainer(cid.get(), options);
61
62 if (getContainerIdFile().isPresent())
63 getContainerIdFile().getAsFile().get().delete();
34 exec(docker().containerStop(getContainerId().get(), options));
64 35 }
65 36
66 37 }
@@ -1,47 +1,44
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 4 import java.util.Set;
5 5 import java.util.concurrent.ExecutionException;
6 6
7 7 import org.gradle.api.provider.Property;
8 8 import org.gradle.api.provider.SetProperty;
9 9 import org.gradle.api.tasks.Input;
10 10 import org.gradle.api.tasks.TaskAction;
11 11 import org.implab.gradle.containers.cli.Utils;
12 12
13 13 public abstract class TagImage extends DockerCliTask {
14 14 @Input
15 15 public abstract Property<Object> getSrcImage();
16 16
17 17 @Input
18 18 public abstract SetProperty<Object> getTags();
19 19
20 20 @Input
21 21 @Deprecated
22 22 public abstract Property<Object> getDestImage();
23 23
24 24 private Set<String> getImageTags() {
25 25 var tags = getTags().map(Utils::mapToString).get();
26 26 tags.add(getDestImage().map(Object::toString).get());
27 27 return tags;
28 28 }
29 29
30 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 34 @TaskAction
35 35 public void run() throws InterruptedException, IOException, ExecutionException {
36 36 var tags = getImageTags();
37 37 var src = getSrcImage().map(Object::toString).get();
38 38
39 if (tags.size() == 0)
40 getLogger().info("No tags were specified");
41
42 39 for (var tag : tags) {
43 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