##// END OF EJS Templates
WIP docker exec, run commands
cin -
r18:ec532699ca7d default
parent child
Show More
@@ -0,0 +1,106
1 package org.implab.gradle.containers.cli;
2
3 import java.io.File;
4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Collection;
7 import java.util.List;
8 import java.util.concurrent.CompletableFuture;
9 import java.util.concurrent.ExecutionException;
10
11 public class ProcessSpec {
12 private final ProcessBuilder builder;
13
14 private RedirectFrom inputRedirect;
15
16 private RedirectTo outputRedirect;
17
18 private RedirectTo errorRedirect;
19
20 private List<String> command = new ArrayList<>();
21
22 private File directory;
23
24 public ProcessSpec() {
25 builder = new ProcessBuilder();
26 }
27
28 public CompletableFuture<Integer> start() throws IOException {
29 var tasks = new ArrayList<CompletableFuture<?>>();
30
31 builder.command(this.command);
32 builder.directory(directory);
33
34 var proc = builder.start();
35
36 tasks.add(proc.onExit());
37
38 if (inputRedirect != null)
39 tasks.add(inputRedirect.redirect(proc.getOutputStream()));
40
41 if (outputRedirect != null)
42 tasks.add(outputRedirect.redirect(proc.getInputStream()));
43
44 if (errorRedirect != null)
45 tasks.add(errorRedirect.redirect(proc.getErrorStream()));
46
47 return CompletableFuture
48 .allOf(tasks.toArray(new CompletableFuture<?>[0]))
49 .thenApply(t -> proc.exitValue());
50 }
51
52 public void exec() throws InterruptedException, ExecutionException, IOException {
53 var code = start().get();
54 if (code != 0)
55 throw new IOException("The process exited with error code " + code);
56 }
57
58 public List<String> command() {
59 return this.command;
60 }
61
62 public ProcessSpec command(String... args) {
63 this.command = new ArrayList<>();
64 args(args);
65 return this;
66 }
67
68 public ProcessSpec directory(File workingDir) {
69 this.directory = workingDir;
70 return this;
71 }
72
73 public ProcessSpec command(Collection<String> args) {
74 this.command = new ArrayList<>();
75 args(args);
76 return this;
77 }
78
79 public ProcessSpec args(String... args) {
80 for (String arg : args)
81 this.command.add(arg);
82
83 return this;
84 }
85
86 public ProcessSpec args(Collection<String> args) {
87 this.command.addAll(args);
88
89 return this;
90 }
91
92 public ProcessSpec redirectStderr(RedirectTo to) {
93 this.errorRedirect = to;
94 return this;
95 }
96
97 public ProcessSpec redirectStdin(RedirectFrom from) {
98 this.inputRedirect = from;
99 return this;
100 }
101
102 public ProcessSpec redirectStdout(RedirectTo to) {
103 this.outputRedirect = to;
104 return this;
105 }
106 }
@@ -0,0 +1,32
1 package org.implab.gradle.containers.cli;
2
3 import java.io.File;
4 import java.io.FileInputStream;
5 import java.io.InputStream;
6 import java.io.OutputStream;
7 import java.util.concurrent.Callable;
8 import java.util.concurrent.CompletableFuture;
9
10 public interface RedirectFrom {
11 CompletableFuture<Void> redirect(OutputStream to);
12
13 public static RedirectFrom file(final File file) {
14 return to -> CompletableFuture.runAsync(() -> {
15 try (var from = new FileInputStream(file); to) {
16 from.transferTo(to);
17 } catch (Exception e) {
18 // silence!
19 }
20 });
21 }
22
23 public static RedirectFrom stream(final InputStream from) {
24 return to -> CompletableFuture.runAsync(() -> {
25 try (from; to) {
26 from.transferTo(to);
27 } catch (Exception e) {
28 // silence!
29 }
30 });
31 }
32 }
@@ -0,0 +1,47
1 package org.implab.gradle.containers.cli;
2
3 import java.io.File;
4 import java.io.FileOutputStream;
5 import java.io.InputStream;
6 import java.io.OutputStream;
7 import java.util.Scanner;
8 import java.util.concurrent.CompletableFuture;
9 import java.util.function.Consumer;
10
11 /**
12 * RedirectSpec
13 */
14 public interface RedirectTo {
15 CompletableFuture<Void> redirect(InputStream from);
16
17 public static RedirectTo consumer(final Consumer<String> consumer) {
18
19 return (src) -> CompletableFuture.runAsync(() -> {
20 try (Scanner sc = new Scanner(src)) {
21 while (sc.hasNextLine()) {
22 consumer.accept(sc.nextLine());
23 }
24 }
25 });
26 }
27
28 public static RedirectTo file(final File file) {
29 return src -> CompletableFuture.runAsync(() -> {
30 try (OutputStream out = new FileOutputStream(file)) {
31 src.transferTo(out);
32 } catch (Exception e) {
33 // silence!
34 }
35 });
36 }
37
38 public static RedirectTo stream(final OutputStream dest) {
39 return src -> CompletableFuture.runAsync(() -> {
40 try (dest; src) {
41 src.transferTo(dest);
42 } catch (Exception e) {
43 // silence!
44 }
45 });
46 }
47 } No newline at end of file
@@ -0,0 +1,37
1 package org.implab.gradle.containers.dsl;
2
3 import java.io.File;
4 import java.io.InputStream;
5 import java.util.Optional;
6 import java.util.function.Supplier;
7
8 import org.gradle.api.provider.Provider;
9 import org.implab.gradle.containers.cli.RedirectFrom;
10
11 public class RedirectFromSpec {
12 private Supplier<RedirectFrom> streamRedirect;
13
14 public boolean isRedirected() {
15 return streamRedirect != null;
16 }
17
18 public Optional<RedirectFrom> getRedirection() {
19 return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty();
20 }
21
22 public void fromFile(File file) {
23 this.streamRedirect = () -> RedirectFrom.file(file);
24 }
25
26 public void fromFile(Provider<File> file) {
27 this.streamRedirect = file.map(RedirectFrom::file)::get;
28 }
29
30 public void fromStream(InputStream stream) {
31 this.streamRedirect = () -> RedirectFrom.stream(stream);
32 }
33
34 public void fromStream(Provider<InputStream> stream) {
35 this.streamRedirect = stream.map(RedirectFrom::stream)::get;
36 }
37 }
@@ -0,0 +1,42
1 package org.implab.gradle.containers.dsl;
2
3 import java.io.File;
4 import java.io.OutputStream;
5 import java.util.Optional;
6 import java.util.function.Consumer;
7 import java.util.function.Supplier;
8
9 import org.gradle.api.provider.Provider;
10 import org.implab.gradle.containers.cli.RedirectTo;
11
12 public class RedirectToSpec {
13 private Supplier<RedirectTo> streamRedirect;
14
15 public boolean isRedirected() {
16 return streamRedirect != null;
17 }
18
19 public Optional<RedirectTo> getRedirection() {
20 return streamRedirect != null ? Optional.ofNullable(streamRedirect.get()) : Optional.empty();
21 }
22
23 public void toFile(File file) {
24 this.streamRedirect = () -> RedirectTo.file(file);
25 }
26
27 public void toFile(Provider<File> file) {
28 this.streamRedirect = file.map(RedirectTo::file)::get;
29 }
30
31 public void toStream(OutputStream stream) {
32 this.streamRedirect = () -> RedirectTo.stream(stream);
33 }
34
35 public void toStream(Provider<OutputStream> stream) {
36 this.streamRedirect = stream.map(RedirectTo::stream)::get;
37 }
38
39 public void toConsumer(Consumer<String> consumer) {
40 this.streamRedirect = () -> RedirectTo.consumer(consumer);
41 }
42 }
@@ -0,0 +1,10
1 package org.implab.gradle.containers.tasks;
2
3 import org.gradle.api.provider.Property;
4 import org.gradle.api.tasks.Internal;
5
6 public abstract class ComposeExec {
7
8 @Internal
9 public abstract Property<String> getServiceName();
10 }
@@ -0,0 +1,66
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.IOException;
4 import java.util.ArrayList;
5 import java.util.List;
6 import java.util.concurrent.ExecutionException;
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;
12 import org.gradle.api.provider.Provider;
13 import org.gradle.api.tasks.Internal;
14 import org.gradle.api.tasks.TaskAction;
15 import org.implab.gradle.containers.cli.Utils;
16
17 public abstract class StopContainer extends DockerCliTask {
18
19 @Internal
20 public abstract RegularFileProperty getContainerIdFile();
21
22 @Internal
23 public abstract Property<String> getContainerName();
24
25 @Internal
26 public abstract Property<String> getStopSignal();
27
28 @Internal
29 public abstract Property<Integer> getStopTimeout();
30
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 });
41 }
42
43 @TaskAction
44 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>();
53
54 if (getStopSignal().isPresent())
55 options.addAll(List.of("--signal", getStopSignal().get()));
56
57 if (getStopTimeout().isPresent())
58 options.addAll(List.of("--time", getStopTimeout().map(Object::toString).get()));
59
60 docker().stopContainer(cid.get(), options);
61
62 if (getContainerIdFile().isPresent())
63 getContainerIdFile().getAsFile().get().delete();
64 }
65
66 }
@@ -1,2 +1,2
1 1 group=org.implab.gradle
2 version=1.2.2 No newline at end of file
2 version=1.3.0 No newline at end of file
@@ -1,167 +1,169
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 import java.util.Optional;
7 6
8 7 import org.gradle.api.DefaultTask;
9 8 import org.gradle.api.Plugin;
10 9 import org.gradle.api.Project;
11 10 import org.gradle.api.artifacts.Configuration;
12 11 import org.gradle.api.artifacts.Configuration.State;
13 12 import org.gradle.api.logging.Logger;
14 13 import org.gradle.api.tasks.Copy;
15 14 import org.gradle.api.tasks.Delete;
16 15 import org.implab.gradle.containers.cli.Utils;
17 16 import org.implab.gradle.containers.tasks.ComposeRm;
18 17 import org.implab.gradle.containers.tasks.ComposeStop;
19 18 import org.implab.gradle.containers.tasks.ComposeUp;
20 19 import org.implab.gradle.containers.tasks.WriteEnv;
21 20
22 21 public abstract class ComposePlugin implements Plugin<Project>, ProjectMixin {
22 public static final String BUILD_GROUP = "build";
23
23 24 public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages";
24 25
25 26 public final String COMPOSE_PROJECT_NAME = "COMPOSE_PROJECT_NAME";
26 27
27 28 public final String COMPOSE_EXTENSION = "compose";
28 29
29 30 public final String COMPOSE_UP_TASK = "up";
30 31
31 32 public final String COMPOSE_STOP_TASK = "stop";
32 33
33 34 public final String COMPOSE_RM_TASK = "rm";
34 35
35 36 public final String CLEAN_TASK = "clean";
36 37
37 38 public final String BUILD_TASK = "build";
38 39
39 40 public final String PROCESS_RESOURCES_TASK = "processResources";
40 41
41 42 public final String WRITE_ENV_TASK = "writeEnv";
42 43
43 44 public final String COMPOSE_VAR = "composeVar";
44 45
45 46 public final String ENV_FILE_NAME = ".env";
46 47
47 48 public Logger getLogger() {
48 49 return getProject().getLogger();
49 50 }
50 51
51 52 @Override
52 53 public void apply(Project project) {
53 54 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
54 55
55 56 // basic configuration, register extension
56 57 var basePlugin = plugin(ContainerBasePlugin.class);
57 58 var containerExtension = basePlugin.getContainerExtension();
58 59
59 60 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
60 61
61 62 var composeFile = containerExtension.getContextDirectory()
62 63 .file(composeExtension.getComposeFileName());
63 64 var composeProfiles = composeExtension.getProfiles();
64 65
65 66 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
66 67 t.delete(containerExtension.getContextDirectory());
67 68 });
68 69
69 70 // copy task from src/main
70 71 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
71 72 t.mustRunAfter(cleanTask);
72 73 t.from(projectDirectory().dir("src/main"));
73 74 t.into(containerExtension.getContextDirectory());
74 75 });
75 76
76 77 // write .env
77 78 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
78 79 t.dependsOn(processResources, containerImages);
79 80 t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME));
80 81
81 82 var group = project.getGroup();
82 83 if (group != null && group.toString().length() > 0) {
83 84 t.getEnvironment().put(COMPOSE_PROJECT_NAME, group.toString());
84 85 }
85 86
86 87 t.getEnvironment().putAll(containerImages.map(this::extractComposeEnv));
87 88
88 89 });
89 90
90 91 var buildTask = task(BUILD_TASK, DefaultTask.class, t -> {
92 t.setGroup(BUILD_GROUP);
91 93 t.dependsOn(writeEnvTask);
92 94 });
93 95
94 96 var stopTask = task(COMPOSE_STOP_TASK, ComposeStop.class, t -> {
95 97 // stop must run after build
96 98 t.mustRunAfter(buildTask);
97 99
98 100 t.getProfiles().addAll(composeProfiles);
99 101 t.getComposeFile().set(composeFile);
100 102 });
101 103
102 104 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
103 105 // rm must run after build and stop
104 106 t.mustRunAfter(buildTask, stopTask);
105 107
106 108 t.getProfiles().addAll(composeProfiles);
107 109 t.getComposeFile().set(composeFile);
108 110 });
109 111
110 112 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
111 113 t.dependsOn(buildTask);
112 114 // up must run after stop and rm
113 115 t.mustRunAfter(stopTask, rmTask);
114 116
115 117 t.getProfiles().addAll(composeProfiles);
116 118 t.getComposeFile().set(composeFile);
117 119 });
118 120 }
119 121
120 122 /**
121 123 * Processed the configurations, extracts composeVar extra property from
122 124 * each dependency in this configuration and adds a value to the resulting
123 125 * map. The values in the nap will contain image tags.
124 126 */
125 127 private Map<String, String> extractComposeEnv(Configuration config) {
126 128 if (config.getState() != State.UNRESOLVED) {
127 129 getLogger().error("extractComposeEnv: The configuration {} isn't resolved.", config.getName());
128 130 throw new IllegalStateException("The specified configuration isn't resolved");
129 131 }
130 132
131 133 getLogger().info("extractComposeEnv {}", config.getName());
132 134
133 135 var map = new HashMap<String, String>();
134 136
135 137 for (var dependency : config.getDependencies()) {
136 138 // get extra composeVar if present
137 139 extra(dependency, COMPOSE_VAR, String.class).optional().ifPresent(varName -> {
138 140 // if we have a composeVar extra attribute on this dependency
139 141
140 142 // get files for the dependency
141 143 var files = config.files(dependency);
142 144 if (files.size() == 1) {
143 145 // should bw exactly 1 file
144 146 var file = files.stream().findAny().get();
145 147 getLogger().info("Processing {}: {}", dependency, file);
146 148
147 149 try {
148 150 // try read imageRef
149 151 Utils.readImageRef(file).getTag()
150 152 .ifPresentOrElse(
151 153 tag -> map.put(varName, tag),
152 154 () -> getLogger().error("ImageRef doesn't have a tag: {}", file));
153 155
154 156 } catch (IOException e) {
155 157 getLogger().error("Failed to read ImageRef {}: {}", file, e);
156 158 }
157 159
158 160 } else {
159 161 getLogger().warn("Dependency {} must have exactly 1 file", dependency);
160 162 }
161 163 });
162 164 }
163 165
164 166 return map;
165 167 }
166 168
167 169 }
@@ -1,54 +1,56
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 7 import org.implab.gradle.containers.tasks.PushImage;
8 import org.implab.gradle.containers.tasks.RunImage;
8 import org.implab.gradle.containers.tasks.RunContainer;
9 9 import org.implab.gradle.containers.tasks.SaveImage;
10 import org.implab.gradle.containers.tasks.StopContainer;
10 11 import org.implab.gradle.containers.tasks.TagImage;
11 12
12 13 public class ContainerBasePlugin implements Plugin<Project> {
13 14 public static final String CONTAINER_EXTENSION_NAME = "container";
14 15
15 16 private ContainerExtension containerExtension;
16 17
17 18 ContainerExtension getContainerExtension() {
18 19 if (containerExtension == null)
19 20 throw new IllegalStateException();
20 21 return containerExtension;
21 22 }
22 23
23 24 void exportClasses(Project project, Class<?>... classes) {
24 25 ExtraPropertiesExtension extras = project.getExtensions().getExtraProperties();
25 26 for (var clazz : classes)
26 27 extras.set(clazz.getSimpleName(), clazz);
27 28 }
28 29
29 30 @Override
30 31 public void apply(Project project) {
31 32
32 33 containerExtension = project.getObjects().newInstance(ContainerExtension.class);
33 34
34 35 // TODO: move properties initialization into the constructor
35 36 containerExtension.getImageAuthority()
36 37 .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority")));
37 38
38 39 containerExtension.getImageGroup()
39 40 .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup")));
40 41
41 42 containerExtension.getCliCmd()
42 43 .convention(project
43 44 .provider(() -> (String) project.getProperties().get("containerCli"))
44 45 .orElse("docker"));
45 46
46 47 project.getExtensions().add(CONTAINER_EXTENSION_NAME, containerExtension);
47 48
48 49 exportClasses(
49 50 project,
50 BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunImage.class);
51 BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunContainer.class,
52 StopContainer.class);
51 53
52 54 }
53 55
54 56 }
@@ -1,186 +1,162
1 1 package org.implab.gradle.containers.cli;
2 2
3 3 import java.io.File;
4 4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 5 import java.util.List;
8 6 import java.util.Optional;
9 7 import java.util.Set;
8 import java.util.concurrent.ExecutionException;
10 9
11 10 import org.gradle.api.logging.Logger;
12 11
13 12 public abstract class DockerTraits {
14 13
15 14 public final String BUILD_COMMAND = "build";
16 15 public final String PUSH_COMMAND = "push";
17 16 public final String RUN_COMMAND = "run";
18 17 public final String SAVE_COMMAND = "save";
19 18 public final String INSPECT_COMMAND = "inspect";
20 19 public final String IMAGE_COMMAND = "image";
21 20 public final String TAG_COMMAND = "tag";
22 21 public final String COMPOSE_COMMAND = "compose";
23 22 public final String UP_COMMAND = "up";
24 23 public final String STOP_COMMAND = "stop";
25 24 public final String RM_COMMAND = "rm";
26 25
27 26 public abstract Logger getLogger();
28 27
29 28 public abstract Optional<File> getWorkingDir();
30 29
31 30 public abstract String getCliCmd();
32 31
33 Process startProcess(ProcessBuilder builder) throws IOException {
34 getLogger().info("Starting: {}", builder.command());
35 return builder.start();
36 }
37
38 protected boolean checkRetCode(Process proc, int code) throws InterruptedException {
32 protected boolean checkRetCode(ProcessSpec proc, int code)
33 throws InterruptedException, ExecutionException, IOException {
39 34 if (getLogger().isInfoEnabled()) {
40 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
41 Utils.redirectIO(proc.getErrorStream(), getLogger()::info);
35 proc.redirectStdout(RedirectTo.consumer(getLogger()::info))
36 .redirectStderr(RedirectTo.consumer(getLogger()::info));
42 37 }
43 38
44 return proc.waitFor() == code;
39 getLogger().info("Starting: {}", proc.command());
40
41 return proc.start().get() == code;
45 42 }
46 43
47 protected void complete(Process proc) throws InterruptedException, IOException {
44 protected void exec(ProcessSpec proc) throws InterruptedException, IOException, ExecutionException {
48 45 if (getLogger().isInfoEnabled())
49 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
46 proc.redirectStdout(RedirectTo.consumer(getLogger()::info));
50 47
51 48 if (getLogger().isErrorEnabled())
52 Utils.redirectIO(proc.getErrorStream(), getLogger()::error);
49 proc.redirectStderr(RedirectTo.consumer(getLogger()::error));
53 50
54 var code = proc.waitFor();
51 getLogger().info("Starting: {}", proc.command());
52
53 var code = proc.start().get();
55 54 if (code != 0) {
56 55 getLogger().error("The process exited with code {}", code);
57 56 throw new IOException("The process exited with error code " + code);
58 57 }
59 58 }
60 59
61 protected ProcessBuilder builder(String... args) {
62 var argsList = new ArrayList<String>(args.length + 1);
63 Arrays.stream(args).forEach(argsList::add);
64
65 return builder(argsList);
66 }
60 protected ProcessSpec builder(String... args) {
61 var spec = new ProcessSpec().args(getCliCmd()).args(args);
67 62
68 protected ProcessBuilder builder(List<String> args) {
69 var command = new ArrayList<String>(args.size() + 1);
63 getWorkingDir().ifPresent(spec::directory);
70 64
71 command.add(getCliCmd());
72 args.forEach(command::add);
73
74 var builder = new ProcessBuilder(command);
75
76 getWorkingDir().ifPresent(builder::directory);
77 return builder;
65 return spec;
78 66 }
79 67
80 68 public void buildImage(String imageName, File contextDirectory, List<String> options)
81 throws IOException, InterruptedException {
82 var args = new ArrayList<String>();
83 args.add(BUILD_COMMAND);
84 args.addAll(options);
85 args.add("-t");
86 args.add(imageName);
87 args.add(contextDirectory.getAbsolutePath());
88 complete(startProcess(builder(args)));
69 throws IOException, InterruptedException, ExecutionException {
70 var spec = builder(BUILD_COMMAND)
71 .args(options)
72 .args("-t", imageName, contextDirectory.getAbsolutePath());
73
74 exec(spec);
89 75 }
90 76
91 public void pushImage(String image, List<String> options) throws InterruptedException, IOException {
92 var args = new ArrayList<String>();
93 args.add(PUSH_COMMAND);
94 args.addAll(options);
95 args.add(image);
96 complete(startProcess(builder(args)));
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);
97 84 }
98 85
99 public void runImage(String image, List<String> options, List<String> command)
86 public ProcessSpec runImage(String image, List<String> options, List<String> command)
100 87 throws InterruptedException, IOException {
101 var args = new ArrayList<String>();
102 args.add(RUN_COMMAND);
103 args.addAll(options);
104 args.add(image);
105 args.addAll(command);
106 complete(startProcess(builder(args)));
88 return builder(RUN_COMMAND)
89 .args(options)
90 .args(image)
91 .args(command);
107 92 }
108 93
109 public void saveImage(Set<String> images, File output) throws InterruptedException, IOException {
94 public void stopContainer(String containerId, List<String> options) throws InterruptedException, IOException, ExecutionException {
95 exec(builder(STOP_COMMAND, containerId).args(options));
96 }
97
98 public void saveImage(Set<String> images, File output)
99 throws InterruptedException, IOException, ExecutionException {
110 100 if (output.exists())
111 101 output.delete();
112 102
113 var args = new ArrayList<String>();
114 args.add(SAVE_COMMAND);
115 args.add("-o");
116 args.add(output.getAbsolutePath());
117 images.forEach(args::add);
103 var spec = builder(SAVE_COMMAND)
104 .args("-o", output.getAbsolutePath())
105 .args(images);
118 106
119 complete(startProcess(builder(args)));
107 exec(spec);
120 108 }
121 109
122 public void tagImage(String source, String target) throws InterruptedException, IOException {
123 complete(startProcess(builder(TAG_COMMAND, source, target)));
110 public void tagImage(String source, String target) throws InterruptedException, IOException, ExecutionException {
111 exec(builder(TAG_COMMAND, source, target));
124 112 }
125 113
126 public boolean imageExists(String imageId) throws InterruptedException, IOException {
114 public boolean imageExists(String imageId) throws InterruptedException, IOException, ExecutionException {
127 115 getLogger().info("Check image {} exists", imageId);
128 116
129 117 return checkRetCode(
130 startProcess(builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId)),
118 builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId),
131 119 0);
132 120 }
133 121
134 122 public boolean imageExists(File imageIdFile) {
135 123 if (imageIdFile.exists()) {
136 124 try {
137 125 var imageId = Utils.readImageRef(imageIdFile);
138 126 return imageExists(imageId.getId());
139 } catch (IOException | InterruptedException e) {
127 } catch (IOException | InterruptedException | ExecutionException e) {
140 128 getLogger().error("Failed to read imageId {}: {}", imageIdFile, e);
141 129 return false;
142 130 }
143 131 }
144 132 return false;
145 133 }
146 134
147 List<String> composeArgs(File primaryCompose, Set<String> profiles, String... extra) {
148 var args = new ArrayList<String>();
135 ProcessSpec compose(File primaryCompose, Set<String> profiles, String... extra) {
136 var spec = builder(COMPOSE_COMMAND, "--file", primaryCompose.getAbsolutePath());
149 137
150 args.add(COMPOSE_COMMAND);
151 args.add("--file");
152 args.add(primaryCompose.getAbsolutePath());
138 for (var profile : profiles)
139 spec.args("--profile", profile);
153 140
154 if (profiles.size() > 0) {
155 for (var profile : profiles) {
156 args.add("--profile");
157 args.add(profile);
158 }
141 spec.args(extra);
142
143 return spec;
159 144 }
160 145
161 args.addAll(List.of(extra));
162
163 return args;
146 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
147 exec(compose(primaryCompose, profiles, UP_COMMAND, "--detach"));
164 148 }
165 149
166 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
167 var args = composeArgs(primaryCompose, profiles, UP_COMMAND, "--detach");
168
169 complete(startProcess(builder(args)));
170 }
171
172 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
173 var args = composeArgs(primaryCompose, profiles, STOP_COMMAND);
174 complete(startProcess(builder(args)));
150 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException, ExecutionException {
151 exec(compose(primaryCompose, profiles, STOP_COMMAND));
175 152 }
176 153
177 154 public void composeRm(File primaryCompose, Set<String> profiles, boolean removeVolumes)
178 throws InterruptedException, IOException {
179 var args = composeArgs(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
180
155 throws InterruptedException, IOException, ExecutionException {
156 var spec = compose(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
181 157 if (removeVolumes)
182 args.add("--volumes");
158 spec.args("--volumes");
183 159
184 complete(startProcess(builder(args)));
160 exec(spec);
185 161 }
186 162 }
@@ -1,139 +1,156
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 import java.util.Set;
15 import java.util.concurrent.CompletableFuture;
16 import java.util.stream.Collectors;
14 17 import java.util.stream.StreamSupport;
15 18 import java.nio.file.Files;
16 19 import java.util.List;
17 20 import org.gradle.api.Action;
18 21 import org.gradle.internal.impldep.org.bouncycastle.util.Iterable;
19 22
20 23 import com.fasterxml.jackson.core.exc.StreamWriteException;
21 24 import com.fasterxml.jackson.databind.DatabindException;
22 25 import com.fasterxml.jackson.databind.ObjectMapper;
23 26 import com.fasterxml.jackson.databind.SerializationFeature;
24 27 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
25 28
26 29 import groovy.json.JsonGenerator;
27 30 import groovy.json.JsonOutput;
28 31 import groovy.json.JsonGenerator.Converter;
29 32 import groovy.lang.Closure;
30 33
31 34 public final class Utils {
32 public static void redirectIO(final InputStream src, final Action<String> consumer) {
33 new Thread(() -> {
35 public static CompletableFuture<Void> redirectIO(final InputStream src, final Action<String> consumer) {
36 return CompletableFuture.runAsync(() -> {
34 37 try (Scanner sc = new Scanner(src)) {
35 38 while (sc.hasNextLine()) {
36 39 consumer.execute(sc.nextLine());
37 40 }
38 41 }
39 }).start();
42 });
40 43 }
41 44
42 public static void redirectIO(final InputStream src, final File file) {
43 new Thread(() -> {
45 public static CompletableFuture<Void> redirectIO(final InputStream src, final File file) {
46 return CompletableFuture.runAsync(() -> {
44 47 try (OutputStream out = new FileOutputStream(file)) {
45 48 src.transferTo(out);
46 49 } catch (Exception e) {
47 50 // silence!
48 51 }
49 }).start();
52 });
53 }
54
55 public static CompletableFuture<Void> redirectIO(final InputStream src, final OutputStream dst) {
56 return CompletableFuture.runAsync(() -> {
57 try (dst) {
58 src.transferTo(dst);
59 } catch (Exception e) {
60 // silence!
61 }
62 });
50 63 }
51 64
52 65 public static void closeSilent(Closeable handle) {
53 66 try {
54 67 handle.close();
55 68 } catch (Exception e) {
56 69 // silence!
57 70 }
58 71 }
59 72
60 73 public static String readAll(final InputStream src) throws IOException {
61 74 ByteArrayOutputStream out = new ByteArrayOutputStream();
62 75 src.transferTo(out);
63 76 return out.toString();
64 77 }
65 78
66 79 public static <T> T readJson(final Reader reader, Class<T> type) throws IOException {
67 80 ObjectMapper objectMapper = new ObjectMapper()
68 81 .registerModule(new Jdk8Module());
69 82 return objectMapper.readValue(reader, type);
70 83 }
71 84
72 85 public static void writeJson(final File file, Object value)
73 86 throws StreamWriteException, DatabindException, IOException {
74 87 ObjectMapper objectMapper = new ObjectMapper()
75 88 .enable(SerializationFeature.INDENT_OUTPUT)
76 89 .registerModule(new Jdk8Module());
77 90 objectMapper.writeValue(file, value);
78 91 }
79 92
80 93 public static ImageRef readImageRef(final File file) throws FileNotFoundException, IOException {
81 94 try (var reader = new FileReader(file)) {
82 95 return readJson(reader, ImageRef.class);
83 96 }
84 97 }
85 98
86 99 public static String readAll(final File src) throws IOException {
87 100 return Files.readString(src.toPath());
88 101 }
89 102
90 103 public static List<String> readAll(final Iterable<? extends File> files) {
91 104 return StreamSupport.stream(files.spliterator(), false)
92 105 .map(file -> {
93 106 try {
94 107 return Utils.readAll(file);
95 108 } catch (IOException e) {
96 109 throw new RuntimeException(e);
97 110 }
98 111 })
99 112 .toList();
100 113 }
101 114
102 115 public static String readAll(final InputStream src, String charset) throws IOException {
103 116 ByteArrayOutputStream out = new ByteArrayOutputStream();
104 117 src.transferTo(out);
105 118 return out.toString(charset);
106 119 }
107 120
108 121 public static JsonGenerator createDefaultJsonGenerator() {
109 122 return new JsonGenerator.Options()
110 123 .excludeNulls()
111 124 .addConverter(new Converter() {
112 125 public boolean handles(Class<?> type) {
113 126 return (File.class == type);
114 127 }
115 128
116 129 public Object convert(Object value, String key) {
117 130 return ((File) value).getPath();
118 131 }
119 132 })
120 133 .build();
121 134 }
122 135
123 136 public static String toJsonPretty(Object value) {
124 137 return JsonOutput.prettyPrint(createDefaultJsonGenerator().toJson(value));
125 138 }
126 139
127 140 public static boolean isNullOrEmptyString(String value) {
128 141 return (value == null || value.length() == 0);
129 142 }
130 143
131 144 public static <T> Action<T> wrapClosure(Closure<?> closure) {
132 145 return x -> {
133 146 closure.setDelegate(x);
134 147 closure.setResolveStrategy(Closure.OWNER_FIRST);
135 148 closure.call(x);
136 149 };
137 150 }
138 151
152 public static Set<String> mapToString(Set<Object> set) {
153 return set.stream().map(Object::toString).collect(Collectors.toSet());
154 }
155
139 156 } No newline at end of file
@@ -1,116 +1,116
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 24
25 25 import org.implab.gradle.containers.cli.ImageRef;
26 26 import org.implab.gradle.containers.cli.Utils;
27 27 import org.implab.gradle.containers.dsl.MapPropertyEntry;
28 28 import org.implab.gradle.containers.dsl.OptionsMixin;
29 29
30 30 import groovy.lang.Closure;
31 31
32 32 public abstract class BuildImage extends DockerCliTask implements OptionsMixin {
33 33
34 34 @InputDirectory
35 35 @SkipWhenEmpty
36 36 public abstract DirectoryProperty getContextDirectory();
37 37
38 38 @Input
39 39 public abstract MapProperty<String, String> getBuildArgs();
40 40
41 41 @Input
42 42 @Optional
43 43 public abstract Property<String> getBuildTarget();
44 44
45 45 @Input
46 public abstract Property<String> getImageName();
46 public abstract Property<Object> getImageName();
47 47
48 48 @Internal
49 49 public abstract RegularFileProperty getImageIdFile();
50 50
51 51 @OutputFile
52 52 public Provider<File> getImageIdFileOutput() {
53 53 return getImageIdFile().map(RegularFile::getAsFile);
54 54 }
55 55
56 56 public BuildImage() {
57 57 getOutputs().upToDateWhen(task -> getImageIdFile()
58 58 .map(RegularFile::getAsFile)
59 59 .map(docker()::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 80 @TaskAction
81 81 public void run() throws Exception {
82 82 List<String> args = new ArrayList<>();
83 83
84 84 // create a temp file to store image id
85 85 var iidFile = new File(this.getTemporaryDir(), "iid");
86 86
87 87 // specify where to write image id
88 88 args.addAll(List.of("--iidfile", iidFile.getAbsolutePath()));
89 89
90 90 getBuildArgs().get().forEach((k, v) -> {
91 91 args.add("--build-arg");
92 92 args.add(String.format("%s=%s", k, v));
93 93 });
94 94
95 95 // add --target if specified for multi-stage build
96 96 if (getBuildTarget().isPresent()) {
97 97 args.add("--target");
98 98 args.add(getBuildTarget().get());
99 99 }
100 100
101 101 // add extra parameters
102 102 getOptions().get().forEach(args::add);
103 103
104 var imageTag = getImageName().get();
104 var imageTag = getImageName().map(Object::toString).get();
105 105
106 106 // build image
107 107 docker().buildImage(
108 108 imageTag,
109 109 getContextDirectory().map(Directory::getAsFile).get(),
110 110 args);
111 111
112 112 // read the built image id and store image ref metadata
113 113 var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile));
114 114 Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef);
115 115 }
116 116 }
@@ -1,22 +1,23
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 5
5 6 import org.gradle.api.provider.Property;
6 7 import org.gradle.api.tasks.Internal;
7 8 import org.gradle.api.tasks.TaskAction;
8 9
9 public abstract class ComposeRm extends ComposeStop {
10 public abstract class ComposeRm extends ComposeTask {
10 11
11 12 @Internal
12 13 public abstract Property<Boolean> getRemoveVolumes();
13 14
14 15 public ComposeRm() {
15 16 getRemoveVolumes().convention(true);
16 17 }
17 18
18 19 @TaskAction
19 public void run() throws InterruptedException, IOException {
20 public void run() throws InterruptedException, IOException, ExecutionException {
20 21 docker().composeRm(getComposeFile().get().getAsFile(), getProfiles().get(), getRemoveVolumes().get());
21 22 }
22 23 }
@@ -1,13 +1,14
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 5
5 6 import org.gradle.api.tasks.TaskAction;
6 7
7 8 public abstract class ComposeStop extends ComposeTask {
8 9
9 10 @TaskAction
10 public void run() throws InterruptedException, IOException {
11 public void run() throws InterruptedException, IOException, ExecutionException {
11 12 docker().composeStop(getComposeFile().getAsFile().get(), getProfiles().get());
12 13 }
13 14 } No newline at end of file
@@ -1,13 +1,14
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 5
5 6 import org.gradle.api.tasks.TaskAction;
6 7
7 8 public abstract class ComposeUp extends ComposeTask {
8 9
9 10 @TaskAction
10 public void run() throws InterruptedException, IOException {
11 public void run() throws InterruptedException, IOException, ExecutionException {
11 12 docker().composeUp(getComposeFile().getAsFile().get(), getProfiles().get());
12 13 }
13 14 }
@@ -1,61 +1,89
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.File;
4 4 import java.util.Optional;
5 5
6 6 import org.gradle.api.DefaultTask;
7 7 import org.gradle.api.logging.Logger;
8 8 import org.gradle.api.provider.Property;
9 9 import org.gradle.api.tasks.Input;
10 10 import org.gradle.api.tasks.Internal;
11 11 import org.implab.gradle.containers.ContainerExtension;
12 12 import org.implab.gradle.containers.PropertiesMixin;
13 13 import org.implab.gradle.containers.cli.DockerTraits;
14 import org.implab.gradle.containers.cli.ProcessSpec;
15 import org.implab.gradle.containers.cli.RedirectFrom;
16 import org.implab.gradle.containers.cli.RedirectTo;
14 17
15 18 public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin {
16 19
17 20 @Input
18 21 public abstract Property<String> getCliCmd();
19 22
20 23 /**
21 24 * Returns working directory for docker commands
22 25 */
23 26 @Input
24 27 @org.gradle.api.tasks.Optional
25 28 public abstract Property<File> getWorkingDirectory();
26 29
27 30 @Internal
28 31 protected ContainerExtension getContainerExtension() {
29 32 return getProject().getExtensions().getByType(ContainerExtension.class);
30 33 }
31 34
32 35 public DockerCliTask() {
33 36 getCliCmd().convention(getContainerExtension().getCliCmd());
34 37
35 38 }
36 39
40 protected Optional<RedirectTo> loggerInfoRedirect() {
41 return getLogger().isInfoEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::info)) : Optional.empty();
42 }
43
44 protected Optional<RedirectTo> loggerErrorRedirect() {
45 return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty();
46 }
47
48 protected Optional<RedirectTo> getStdoutRedirection() {
49 return loggerInfoRedirect();
50 }
51
52 protected Optional<RedirectTo> getStderrRedirection() {
53 return loggerErrorRedirect();
54 }
55
56 protected Optional<RedirectFrom> getStdinRedirection() {
57 return Optional.empty();
58 }
59
37 60 protected DockerTraits docker() {
38 61 return new TaskDockerTraits();
39 62 }
40 63
64 protected void exec(ProcessSpec spec) {
65
66 getLogger().info("Starting: {}", spec.command());
67 }
68
41 69 class TaskDockerTraits extends DockerTraits {
42 70
43 71 @Override
44 72 public Logger getLogger() {
45 73 return DockerCliTask.this.getLogger();
46 74 }
47 75
48 76 @Override
49 77 public Optional<File> getWorkingDir() {
50 78 return getWorkingDirectory()
51 79 .map(Optional::of)
52 80 .getOrElse(Optional.empty());
53 81 }
54 82
55 83 @Override
56 84 public String getCliCmd() {
57 85 return DockerCliTask.this.getCliCmd().get();
58 86 }
59 87
60 88 }
61 89 }
@@ -1,32 +1,33
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 import java.util.concurrent.ExecutionException;
4 5
5 6 import org.gradle.api.provider.ListProperty;
6 7 import org.gradle.api.provider.Property;
7 8 import org.gradle.api.tasks.Input;
8 9 import org.gradle.api.tasks.Optional;
9 10 import org.gradle.api.tasks.TaskAction;
10 11 import org.implab.gradle.containers.dsl.OptionsMixin;
11 12
12 13 public abstract class PushImage extends DockerCliTask implements OptionsMixin {
13 14
14 15 @Input
15 public abstract Property<String> getImageName();
16 public abstract Property<Object> getImageName();
16 17
17 18 @Input
18 19 @Optional
19 public abstract Property<String> getTransport();
20 public abstract Property<Object> getTransport();
20 21
21 22 @Input
22 23 @Optional
23 24 public abstract ListProperty<String> getOptions();
24 25
25 26 @TaskAction
26 public void run() throws InterruptedException, IOException {
27 public void run() throws InterruptedException, IOException, ExecutionException {
27 28
28 29 docker().pushImage(
29 getImageName().get(),
30 getImageName().map(Object::toString).get(),
30 31 getOptions().get());
31 32 }
32 33 }
@@ -1,44 +1,159
1 1 package org.implab.gradle.containers.tasks;
2 2
3 import java.io.File;
3 4 import java.io.IOException;
5 import java.util.ArrayList;
6 import java.util.List;
7 import java.util.Optional;
8 import java.util.concurrent.ExecutionException;
9
4 10 import org.gradle.api.Action;
11 import org.gradle.api.file.RegularFileProperty;
5 12 import org.gradle.api.provider.ListProperty;
6 13 import org.gradle.api.provider.Property;
7 import org.gradle.api.tasks.Input;
8 import org.gradle.api.tasks.Optional;
14 import org.gradle.api.tasks.Internal;
9 15 import org.gradle.api.tasks.TaskAction;
16 import org.implab.gradle.containers.cli.RedirectFrom;
17 import org.implab.gradle.containers.cli.RedirectTo;
10 18 import org.implab.gradle.containers.dsl.OptionsMixin;
19 import org.implab.gradle.containers.dsl.RedirectFromSpec;
20 import org.implab.gradle.containers.dsl.RedirectToSpec;
11 21 import org.implab.gradle.containers.dsl.VolumeSpec;
12 22
13 public abstract class RunImage extends DockerCliTask implements OptionsMixin {
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();
14 30
15 @Input
16 public abstract Property<String> getImageName();
31 private boolean transientContainer = true;
32
33 private boolean detached = false;
17 34
18 @Input
19 @Optional
35 private boolean interactive = false;
36
37 @Internal
38 public abstract Property<Object> getImageName();
39
40 @Internal
20 41 public abstract ListProperty<String> getCommandLine();
21 42
43 @Internal
44 public RedirectFromSpec getStdin() {
45 return redirectStdin;
46 }
47
48 @Internal
49 public RedirectToSpec getStdout() {
50 return redirectStdout;
51 }
52
53 @Internal
54 public RedirectToSpec getStderr() {
55 return redirectStderr;
56 }
57
58 @Internal
59 public boolean isTransientContainer() {
60 return transientContainer;
61 }
62
63 public void setTransientContainer(boolean value) {
64 transientContainer = value;
65 }
66
67 @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;
72
73 return detached;
74 }
75
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
86 @Internal
87 public abstract RegularFileProperty getContainerIdFile();
88
89 public RunContainer() {
90 getContainerIdFile().convention(() -> new File(getTemporaryDir(), "cid"));
91 }
22 92
23 93 public void volume(Action<VolumeSpec> configure) {
24 94 getOptions().add("-v");
25 95 getOptions().add(provider(() -> {
26 96 var volumeSpec = getObjectFactory().newInstance(VolumeSpec.class);
27 97 configure.execute(volumeSpec);
28 98 return volumeSpec.resolveSpec();
29 99 }));
30 100 }
31 101
32 102 void commandLine(String ...args) {
33 103 getCommandLine().addAll(args);
34 104 }
35 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
36 121 @TaskAction
37 public void run() throws InterruptedException, IOException {
38 docker().runImage(
39 getImageName().get(),
40 getOptions().get(),
122 public void run() throws InterruptedException, IOException, ExecutionException {
123 var params = new ArrayList<>(getOptions().get());
124
125 if (isInteractive())
126 params.add("--interactive");
127 if (isTransientContainer())
128 params.add("--rm");
129 if (isDetached())
130 params.add("--detach");
131 if (getContainerIdFile().isPresent()) {
132 getContainerIdFile().getAsFile().get().delete();
133
134 params.addAll(List.of("--cidfile", getContainerIdFile().get().getAsFile().toString()));
135 }
136
137 var spec = docker().runImage(
138 getImageName().map(Object::toString).get(),
139 params,
41 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();
42 157 }
43 158
44 159 }
@@ -1,111 +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 import java.util.concurrent.ExecutionException;
7 8
8 9 import org.gradle.api.file.DirectoryProperty;
9 10 import org.gradle.api.file.FileCollection;
10 11 import org.gradle.api.file.RegularFile;
11 12 import org.gradle.api.provider.Property;
12 13 import org.gradle.api.provider.Provider;
13 14 import org.gradle.api.provider.SetProperty;
14 15 import org.gradle.api.tasks.Input;
15 16 import org.gradle.api.tasks.Internal;
16 17 import org.gradle.api.tasks.OutputFile;
17 18 import org.gradle.api.tasks.TaskAction;
18 19 import org.gradle.api.tasks.TaskExecutionException;
19 20 import org.implab.gradle.containers.cli.Utils;
20 21
21 22 public abstract class SaveImage extends DockerCliTask {
22 23
23 24 @Input
24 public abstract SetProperty<String> getExportImages();
25 public abstract SetProperty<Object> getExportImages();
25 26
26 27 @OutputFile
27 28 public Provider<RegularFile> getArchiveFile() {
28 29 return getDestinationDirectory().file(getArchiveFileName());
29 30 }
30 31
31 32 @Internal
32 33 public abstract DirectoryProperty getDestinationDirectory();
33 34
34 35 @Internal
35 36 public abstract Property<String> getArchiveFileName();
36 37
37 38 @Internal
38 39 public abstract Property<String> getArchiveBaseName();
39 40
40 41 @Internal
41 42 public abstract Property<String> getArchiveVersion();
42 43
43 44 @Internal
44 45 public abstract Property<String> getArchiveClassifier();
45 46
46 47 @Internal
47 48 public abstract Property<String> getArchiveExtension();
48 49
49 50 String readImageRefTag(File file) throws Exception {
50 51 getLogger().info("Reading image ref from {}", file);
51 52 var imageRef = Utils.readImageRef(file);
52 53 return imageRef.getTag().orElseThrow(() -> new Exception("The image tag is required to save image"));
53 54 }
54 55
55 56 public void imageRef(File file) {
56 57 getExportImages().add(provider(() -> {
57 58 return readImageRefTag(file);
58 59 }));
59 60 }
60 61
61 62 public void imageRefs(FileCollection files) {
62 63 dependsOn(files);
63 64 getExportImages().addAll(provider(() -> {
64 65 var tags = new ArrayList<String>();
65 66 for (File file : files) {
66 67 tags.add(readImageRefTag(file));
67 68 }
68 69 return tags;
69 70 }));
70 71 }
71 72
72 73 public SaveImage() {
73 74 getArchiveFileName().convention(provider(this::conventionalArchiveFileName));
74 75 getArchiveBaseName().convention(provider(this::conventionalArchiveBaseName));
75 76 getArchiveVersion().convention(provider(() -> getProject().getVersion()).map(x -> x.toString()));
76 77 getDestinationDirectory().convention(getProject().getLayout().getBuildDirectory());
77 78 getArchiveExtension().convention("tar");
78 79 }
79 80
80 81 private String conventionalArchiveBaseName() {
81 82 ArrayList<String> parts = new ArrayList<>();
82 83 Optional.of(getProject().getGroup()).map(x -> x.toString()).ifPresent(parts::add);
83 84 parts.add(getProject().getName());
84 85 return String.join("-", parts);
85 86 }
86 87
87 88 private String conventionalArchiveFileName() {
88 89 ArrayList<String> parts = new ArrayList<>();
89 90
90 91 if (getArchiveBaseName().isPresent())
91 92 parts.add(getArchiveBaseName().get());
92 93
93 94 if (getArchiveVersion().isPresent())
94 95 parts.add(getArchiveVersion().get());
95 96
96 97 if (getArchiveClassifier().isPresent())
97 98 parts.add(getArchiveClassifier().get());
98 99
99 100 if (parts.size() == 0)
100 101 throw new TaskExecutionException(this, new Exception("The archiveFileName ism't specified"));
101 102
102 103 return String.join("-", parts) + "." + getArchiveExtension().get();
103 104 }
104 105
105 106 @TaskAction
106 public void run() throws InterruptedException, IOException {
107 public void run() throws InterruptedException, IOException, ExecutionException {
107 108 docker().saveImage(
108 getExportImages().get(),
109 getExportImages().map(Utils::mapToString).get(),
109 110 getArchiveFile().map(RegularFile::getAsFile).get());
110 111 }
111 112 }
@@ -1,46 +1,47
1 1 package org.implab.gradle.containers.tasks;
2 2
3 3 import java.io.IOException;
4 import java.util.HashSet;
5 4 import java.util.Set;
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 import org.implab.gradle.containers.cli.Utils;
11 12
12 13 public abstract class TagImage extends DockerCliTask {
13 14 @Input
14 public abstract Property<String> getSrcImage();
15 public abstract Property<Object> getSrcImage();
15 16
16 17 @Input
17 public abstract SetProperty<String> getTags();
18 public abstract SetProperty<Object> getTags();
18 19
19 20 @Input
20 21 @Deprecated
21 public abstract Property<String> getDestImage();
22 public abstract Property<Object> getDestImage();
22 23
23 24 private Set<String> getImageTags() {
24 var tags = new HashSet<>(getTags().get());
25 tags.add(getDestImage().get());
25 var tags = getTags().map(Utils::mapToString).get();
26 tags.add(getDestImage().map(Object::toString).get());
26 27 return tags;
27 28 }
28 29
29 30 public TagImage() {
30 31 this.setOnlyIf("No tags were specified", self -> getImageTags().size() > 0);
31 32 }
32 33
33 34 @TaskAction
34 public void run() throws InterruptedException, IOException {
35 public void run() throws InterruptedException, IOException, ExecutionException {
35 36 var tags = getImageTags();
36 var src = getSrcImage().get();
37 var src = getSrcImage().map(Object::toString).get();
37 38
38 39 if (tags.size() == 0)
39 40 getLogger().info("No tags were specified");
40 41
41 42 for (var tag : tags) {
42 43 getLogger().info("Tag: {}", tag);
43 44 docker().tagImage(src, tag);
44 45 }
45 46 }
46 47 }
General Comments 0
You need to be logged in to leave comments. Login now