##// END OF EJS Templates
Fixed docker build output handling. Handled with 'info' log level.
cin -
r21:452b9915903c v1.3.1 default
parent child
Show More
@@ -1,2 +1,2
1 1 group=org.implab.gradle
2 version=1.3.0 No newline at end of file
2 version=1.3.1 No newline at end of file
@@ -1,106 +1,116
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.lang.ProcessBuilder.Redirect;
5 6 import java.util.ArrayList;
6 7 import java.util.Collection;
7 8 import java.util.List;
8 9 import java.util.concurrent.CompletableFuture;
9 10 import java.util.concurrent.ExecutionException;
10 11
11 12 public class ProcessSpec {
12 13 private final ProcessBuilder builder;
13 14
14 15 private RedirectFrom inputRedirect;
15 16
16 17 private RedirectTo outputRedirect;
17 18
18 19 private RedirectTo errorRedirect;
19 20
20 21 private List<String> command = new ArrayList<>();
21 22
22 23 private File directory;
23 24
24 25 public ProcessSpec() {
25 26 builder = new ProcessBuilder();
26 27 }
27 28
28 29 public CompletableFuture<Integer> start() throws IOException {
29 30 var tasks = new ArrayList<CompletableFuture<?>>();
30 31
31 32 builder.command(this.command);
32 33 builder.directory(directory);
33 34
35 // discard stdout if not redirected
36 if (outputRedirect == null)
37 builder.redirectOutput(Redirect.DISCARD);
38
39 // discard stderr if not redirected
40 if (errorRedirect == null)
41 builder.redirectError(Redirect.DISCARD);
42
43 // run process
34 44 var proc = builder.start();
35 45
36 46 tasks.add(proc.onExit());
37 47
38 48 if (inputRedirect != null)
39 49 tasks.add(inputRedirect.redirect(proc.getOutputStream()));
40 50
41 51 if (outputRedirect != null)
42 52 tasks.add(outputRedirect.redirect(proc.getInputStream()));
43 53
44 54 if (errorRedirect != null)
45 55 tasks.add(errorRedirect.redirect(proc.getErrorStream()));
46 56
47 57 return CompletableFuture
48 58 .allOf(tasks.toArray(new CompletableFuture<?>[0]))
49 59 .thenApply(t -> proc.exitValue());
50 60 }
51 61
52 62 public void exec() throws InterruptedException, ExecutionException, IOException {
53 63 var code = start().get();
54 64 if (code != 0)
55 65 throw new IOException("The process exited with error code " + code);
56 66 }
57 67
58 68 public List<String> command() {
59 69 return this.command;
60 70 }
61 71
62 72 public ProcessSpec command(String... args) {
63 73 this.command = new ArrayList<>();
64 74 args(args);
65 75 return this;
66 76 }
67 77
68 78 public ProcessSpec directory(File workingDir) {
69 79 this.directory = workingDir;
70 80 return this;
71 81 }
72 82
73 83 public ProcessSpec command(Collection<String> args) {
74 84 this.command = new ArrayList<>();
75 85 args(args);
76 86 return this;
77 87 }
78 88
79 89 public ProcessSpec args(String... args) {
80 90 for (String arg : args)
81 91 this.command.add(arg);
82 92
83 93 return this;
84 94 }
85 95
86 96 public ProcessSpec args(Collection<String> args) {
87 97 this.command.addAll(args);
88 98
89 99 return this;
90 100 }
91 101
92 102 public ProcessSpec redirectStderr(RedirectTo to) {
93 103 this.errorRedirect = to;
94 104 return this;
95 105 }
96 106
97 107 public ProcessSpec redirectStdin(RedirectFrom from) {
98 108 this.inputRedirect = from;
99 109 return this;
100 110 }
101 111
102 112 public ProcessSpec redirectStdout(RedirectTo to) {
103 113 this.outputRedirect = to;
104 114 return this;
105 115 }
106 116 }
@@ -1,170 +1,169
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 21 import org.gradle.api.GradleException;
22 import org.gradle.api.file.Directory;
23 22 import org.gradle.api.file.FileSystemLocation;
24 23 import org.gradle.internal.impldep.org.bouncycastle.util.Iterable;
25 24
26 25 import com.fasterxml.jackson.core.exc.StreamWriteException;
27 26 import com.fasterxml.jackson.databind.DatabindException;
28 27 import com.fasterxml.jackson.databind.ObjectMapper;
29 28 import com.fasterxml.jackson.databind.SerializationFeature;
30 29 import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
31 30
32 31 import groovy.json.JsonGenerator;
33 32 import groovy.json.JsonOutput;
34 33 import groovy.json.JsonGenerator.Converter;
35 34 import groovy.lang.Closure;
36 35
37 36 public final class Utils {
38 37 public static CompletableFuture<Void> redirectIO(final InputStream src, final Action<String> consumer) {
39 38 return CompletableFuture.runAsync(() -> {
40 39 try (Scanner sc = new Scanner(src)) {
41 40 while (sc.hasNextLine()) {
42 41 consumer.execute(sc.nextLine());
43 42 }
44 43 }
45 44 });
46 45 }
47 46
48 47 public static CompletableFuture<Void> redirectIO(final InputStream src, final File file) {
49 48 return CompletableFuture.runAsync(() -> {
50 49 try (OutputStream out = new FileOutputStream(file)) {
51 50 src.transferTo(out);
52 51 } catch (Exception e) {
53 52 // silence!
54 53 }
55 54 });
56 55 }
57 56
58 57 public static CompletableFuture<Void> redirectIO(final InputStream src, final OutputStream dst) {
59 58 return CompletableFuture.runAsync(() -> {
60 59 try (dst) {
61 60 src.transferTo(dst);
62 61 } catch (Exception e) {
63 62 // silence!
64 63 }
65 64 });
66 65 }
67 66
68 67 public static void closeSilent(Closeable handle) {
69 68 try {
70 69 handle.close();
71 70 } catch (Exception e) {
72 71 // silence!
73 72 }
74 73 }
75 74
76 75 public static String readAll(final InputStream src) throws IOException {
77 76 ByteArrayOutputStream out = new ByteArrayOutputStream();
78 77 src.transferTo(out);
79 78 return out.toString();
80 79 }
81 80
82 81 public static <T> T readJson(final Reader reader, Class<T> type) throws IOException {
83 82 ObjectMapper objectMapper = new ObjectMapper()
84 83 .registerModule(new Jdk8Module());
85 84 return objectMapper.readValue(reader, type);
86 85 }
87 86
88 87 public static void writeJson(final File file, Object value)
89 88 throws StreamWriteException, DatabindException, IOException {
90 89 ObjectMapper objectMapper = new ObjectMapper()
91 90 .enable(SerializationFeature.INDENT_OUTPUT)
92 91 .registerModule(new Jdk8Module());
93 92 objectMapper.writeValue(file, value);
94 93 }
95 94
96 95 public static ImageRef readImageRef(final File file) throws FileNotFoundException, IOException {
97 96 try (var reader = new FileReader(file)) {
98 97 return readJson(reader, ImageRef.class);
99 98 }
100 99 }
101 100
102 101 public static String readAll(final File src) {
103 102 try {
104 103 return src.isFile() ? Files.readString(src.toPath()) : null;
105 104 } catch (IOException e) {
106 105 throw new GradleException("Failed to read file " + src.toString(), e);
107 106 }
108 107 }
109 108
110 109 public static List<String> readAll(final Iterable<? extends File> files) {
111 110 return StreamSupport.stream(files.spliterator(), false)
112 111 .map(Utils::readAll)
113 112 .toList();
114 113 }
115 114
116 115 public static String readAll(final InputStream src, String charset) throws IOException {
117 116 ByteArrayOutputStream out = new ByteArrayOutputStream();
118 117 src.transferTo(out);
119 118 return out.toString(charset);
120 119 }
121 120
122 121 public static JsonGenerator createDefaultJsonGenerator() {
123 122 return new JsonGenerator.Options()
124 123 .excludeNulls()
125 124 .addConverter(new Converter() {
126 125 public boolean handles(Class<?> type) {
127 126 return (File.class == type);
128 127 }
129 128
130 129 public Object convert(Object value, String key) {
131 130 return ((File) value).getPath();
132 131 }
133 132 })
134 133 .build();
135 134 }
136 135
137 136 public static String toJsonPretty(Object value) {
138 137 return JsonOutput.prettyPrint(createDefaultJsonGenerator().toJson(value));
139 138 }
140 139
141 140 public static boolean isNullOrEmptyString(String value) {
142 141 return (value == null || value.length() == 0);
143 142 }
144 143
145 144 public static <T> Action<T> wrapClosure(Closure<?> closure) {
146 145 return x -> {
147 146 closure.setDelegate(x);
148 147 closure.setResolveStrategy(Closure.OWNER_FIRST);
149 148 closure.call(x);
150 149 };
151 150 }
152 151
153 152 public static Set<String> mapToString(Set<Object> set) {
154 153 return set.stream().map(Object::toString).collect(Collectors.toSet());
155 154 }
156 155
157 156 public static File normalizeFile(Object file) {
158 157 if (file == null)
159 158 throw new IllegalArgumentException("file");
160 159
161 160 if (file instanceof String)
162 161 return new File((String)file);
163 162 if (file instanceof FileSystemLocation)
164 163 return ((FileSystemLocation)file).getAsFile();
165 164 else if (file instanceof File)
166 165 return (File)file;
167 166 throw new ClassCastException();
168 167 }
169 168
170 169 } No newline at end of file
@@ -1,132 +1,139
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 import java.io.IOException;
25 25
26 26 import org.implab.gradle.containers.cli.ImageRef;
27 import org.implab.gradle.containers.cli.RedirectTo;
27 28 import org.implab.gradle.containers.cli.Utils;
28 29 import org.implab.gradle.containers.dsl.MapPropertyEntry;
29 30 import org.implab.gradle.containers.dsl.OptionsMixin;
30 31
31 32 import groovy.lang.Closure;
32 33
33 34 public abstract class BuildImage extends DockerCliTask implements OptionsMixin {
34 35
35 36 @InputDirectory
36 37 @SkipWhenEmpty
37 38 public abstract DirectoryProperty getContextDirectory();
38 39
39 40 @Input
40 41 public abstract MapProperty<String, String> getBuildArgs();
41 42
42 43 @Input
43 44 @Optional
44 45 public abstract Property<String> getBuildTarget();
45 46
46 47 @Input
47 48 public abstract Property<Object> getImageName();
48 49
49 50 @Internal
50 51 public abstract RegularFileProperty getImageIdFile();
51 52
52 53 @OutputFile
53 54 public Provider<File> getImageIdFileOutput() {
54 55 return getImageIdFile().map(RegularFile::getAsFile);
55 56 }
56 57
57 58 public BuildImage() {
58 59 getOutputs().upToDateWhen(task -> getImageIdFile()
59 60 .map(this::imageExists)
60 61 .getOrElse(false));
61 62 }
62 63
63 64 public void buildArgs(Action<Map<String, String>> spec) {
64 65 getBuildArgs().putAll(provider(() -> {
65 66 Map<String, String> args = new HashMap<>();
66 67 spec.execute(args);
67 68 getLogger().info("add buildArgs {}", args);
68 69 return args;
69 70 }));
70 71 }
71 72
72 73 public void buildArgs(Closure<Map<String, String>> spec) {
73 74 buildArgs(Utils.wrapClosure(spec));
74 75 }
75 76
76 77 public MapPropertyEntry<String, String> buildArg(String key) {
77 78 return new MapPropertyEntry<String, String>(getBuildArgs(), key, getProviders());
78 79 }
79 80
80 81 private boolean imageExists(RegularFile file) {
81 82 return readRefId(file.getAsFile()).map(this::imageExists).orElse(false);
82 83 }
83 84
84 85 private java.util.Optional<String> readRefId(File idFile) {
85 86 try {
86 87 return idFile.exists() ? java.util.Optional.of(Utils.readImageRef(idFile).getId())
87 88 : java.util.Optional.empty();
88 89 } catch (IOException e) {
89 90 getLogger().error("Failed to read imageId {}: {}", idFile, e);
90 91 return java.util.Optional.empty();
91 92 }
92 93 }
93 94
95 @Override
96 protected java.util.Optional<RedirectTo> loggerErrorRedirect() {
97 // https://github.com/moby/buildkit/issues/1186
98 return loggerInfoRedirect();
99 }
100
94 101 @TaskAction
95 102 public void run() throws Exception {
96 103 List<String> args = new ArrayList<>();
97 104
98 105 // create a temp file to store image id
99 106 var iidFile = new File(this.getTemporaryDir(), "imageid");
100 107
101 108 // specify where to write image id
102 109 args.addAll(List.of("--iidfile", iidFile.getAbsolutePath()));
103 110
104 111 getBuildArgs().get().forEach((k, v) -> {
105 112 args.add("--build-arg");
106 113 args.add(String.format("%s=%s", k, v));
107 114 });
108 115
109 116 // add --target if specified for multi-stage build
110 117 if (getBuildTarget().isPresent()) {
111 118 args.add("--target");
112 119 args.add(getBuildTarget().get());
113 120 }
114 121
115 122 // add extra parameters
116 123 getOptions().get().forEach(args::add);
117 124
118 125 var imageTag = getImageName().map(Object::toString).get();
119 126
120 127 // build image
121 128 var spec = docker().imageBuild(
122 129 imageTag,
123 130 getContextDirectory().map(Directory::getAsFile).get(),
124 131 args);
125 132
126 133 exec(spec);
127 134
128 135 // read the built image id and store image ref metadata
129 136 var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile));
130 137 Utils.writeJson(getImageIdFile().map(RegularFile::getAsFile).get(), imageRef);
131 138 }
132 139 }
@@ -1,150 +1,153
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.Optional;
6 6 import java.util.concurrent.ExecutionException;
7 7
8 8 import org.gradle.api.DefaultTask;
9 9 import org.gradle.api.GradleException;
10 10 import org.gradle.api.file.RegularFile;
11 11 import org.gradle.api.provider.Property;
12 12 import org.gradle.api.tasks.Input;
13 13 import org.gradle.api.tasks.Internal;
14 14 import org.implab.gradle.containers.ContainerExtension;
15 15 import org.implab.gradle.containers.PropertiesMixin;
16 16 import org.implab.gradle.containers.cli.ComposeTraits;
17 17 import org.implab.gradle.containers.cli.DockerTraits;
18 18 import org.implab.gradle.containers.cli.ProcessSpec;
19 19 import org.implab.gradle.containers.cli.RedirectFrom;
20 20 import org.implab.gradle.containers.cli.RedirectTo;
21 21 import org.implab.gradle.containers.cli.Utils;
22 22
23 23 public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin {
24 24
25 25 @Input
26 26 public abstract Property<String> getCliCmd();
27 27
28 28 /**
29 29 * Returns working directory for docker commands
30 30 */
31 31 @Input
32 32 @org.gradle.api.tasks.Optional
33 33 public abstract Property<File> getWorkingDirectory();
34 34
35 35 @Internal
36 36 protected ContainerExtension getContainerExtension() {
37 37 return getProject().getExtensions().getByType(ContainerExtension.class);
38 38 }
39 39
40 40 public DockerCliTask() {
41 41 getCliCmd().convention(getContainerExtension().getCliCmd());
42 42
43 43 }
44 44
45 45 protected Optional<RedirectTo> loggerInfoRedirect() {
46 return getLogger().isInfoEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::info)) : Optional.empty();
46 return getLogger().isInfoEnabled() ?
47 Optional.of(RedirectTo.consumer(getLogger()::info)) :
48 Optional.empty();
47 49 }
48 50
49 51 protected Optional<RedirectTo> loggerErrorRedirect() {
50 return getLogger().isErrorEnabled() ? Optional.of(RedirectTo.consumer(getLogger()::error)) : Optional.empty();
52 return getLogger().isErrorEnabled() ?
53 Optional.of(RedirectTo.consumer(getLogger()::error)) :
54 Optional.empty();
51 55 }
52 56
53 57 protected Optional<RedirectTo> stdoutRedirection() {
54 58 return loggerInfoRedirect();
55 59 }
56 60
57 61 protected Optional<RedirectTo> stderrRedirection() {
58 62 return loggerErrorRedirect();
59 63 }
60 64
61 65 protected Optional<RedirectFrom> stdinRedirection() {
62 66 return Optional.empty();
63 67 }
64 68
65 69 protected TaskDockerTraits docker() {
66 70 return new TaskDockerTraits();
67 71 }
68 72
69 73 protected boolean imageExists(String imageId) {
70 74 try {
71 75 return checkRetCode(docker().imageExists(imageId), 0);
72 76 } catch (InterruptedException | ExecutionException | IOException e) {
73 77 // wrap to unchecked exception
74 78 throw new GradleException("Failed to execute imageExists", e);
75 79 }
76 80 }
77 81
78 82 protected boolean containerExists(String containerId) {
79 83 try {
80 84 return checkRetCode(docker().containerExists(containerId), 0);
81 85 } catch (InterruptedException | ExecutionException | IOException e) {
82 86 // wrap to unchecked exception
83 87 throw new GradleException("Failed to execute imageExists", e);
84 88 }
85 89 }
86 90
87 91 protected void exec(ProcessSpec spec) throws InterruptedException, ExecutionException, IOException {
88
89 92 stdoutRedirection().ifPresent(spec::redirectStdout);
90 93 stderrRedirection().ifPresent(spec::redirectStderr);
91 94 stdinRedirection().ifPresent(spec::redirectStdin);
92 95
93 96 getLogger().info("Staring: {}", spec.command());
94 97
95 98 // runs the command and checks the error code
96 99 spec.exec();
97 100 }
98 101
99 102 protected boolean checkRetCode(ProcessSpec proc, int code)
100 103 throws InterruptedException, ExecutionException, IOException {
101 104 if (getLogger().isInfoEnabled()) {
102 105 proc.redirectStdout(RedirectTo.consumer(getLogger()::info))
103 106 .redirectStderr(RedirectTo.consumer(getLogger()::info));
104 107 }
105 108
106 109 getLogger().info("Starting: {}", proc.command());
107 110
108 111 return proc.start().get() == code;
109 112 }
110 113
111 114 /**
112 115 * Helper function to read
113 116 * @param idFile
114 117 * @return
115 118 */
116 119 protected String readId(File idFile) {
117 120 if (idFile.exists()) {
118 121 try {
119 122 return Utils.readImageRef(idFile).getId();
120 123 } catch (IOException e) {
121 124 getLogger().error("Failed to read imageId {}: {}", idFile, e);
122 125 return null;
123 126 }
124 127 } else {
125 128 return null;
126 129 }
127 130 }
128 131
129 132 protected ProcessSpec commandBuilder(String... command) {
130 133 var spec = new ProcessSpec().args(getCliCmd().get()).args(command);
131 134
132 135 if (getWorkingDirectory().isPresent())
133 136 spec.directory(getWorkingDirectory().get());
134 137
135 138 return spec;
136 139 }
137 140
138 141 class TaskDockerTraits extends DockerTraits {
139 142
140 143 @Override
141 144 protected ProcessSpec builder(String... command) {
142 145 return commandBuilder(command);
143 146 }
144 147
145 148 public ComposeTraits compose(ComposeTaskMixin props) {
146 149 return compose(props.getComposeFile().map(RegularFile::getAsFile).get(), props.getProfiles().get());
147 150 }
148 151
149 152 }
150 153 }
General Comments 0
You need to be logged in to leave comments. Login now