# HG changeset patch # User cin # Date 2023-11-18 16:14:22 # Node ID 04caf9830434485e115ff9b872e4f213f57d5c1c # Parent e992157ede555319307bd9bc68d863c509d865df moving docker commands to DockerTraits, refacotring diff --git a/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java b/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java --- a/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java +++ b/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java @@ -1,19 +1,24 @@ package org.implab.gradle.containers; import java.io.File; +import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.gradle.api.logging.Logger; +import org.gradle.api.tasks.Internal; +import org.implab.gradle.containers.cli.Utils; public interface ExecuteMixin { + @Internal Logger getLogger(); + @Internal Optional getWorkingDir(); - default void Execute() throws Exception { + default void Execute() throws IOException, InterruptedException { final Logger log = getLogger(); List command = new ArrayList<>(); @@ -34,8 +39,12 @@ public interface ExecuteMixin { } int code = p.waitFor(); - if (code != 0) - throw new Exception("Process exited with the error: " + code); + if (code != 0) { + log.error("Process: {}", builder.command()); + log.error("Process exited with code {}", code); + + throw new IOException("Process exited with code: " + code); + } } void preparingCommand(List command); diff --git a/container/src/main/java/org/implab/gradle/containers/Utils.java b/container/src/main/java/org/implab/gradle/containers/Utils.java deleted file mode 100644 --- a/container/src/main/java/org/implab/gradle/containers/Utils.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.implab.gradle.containers; - -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Scanner; - -import org.gradle.api.Action; - -import groovy.json.JsonGenerator; -import groovy.json.JsonOutput; -import groovy.json.JsonGenerator.Converter; -import groovy.lang.Closure; - -public final class Utils { - public static void redirectIO(final InputStream src, final Action consumer) { - new Thread(() -> { - try (Scanner sc = new Scanner(src)) { - while (sc.hasNextLine()) { - consumer.execute(sc.nextLine()); - } - } - }).start(); - } - - public static void redirectIO(final InputStream src, final File file) { - new Thread(() -> { - try (OutputStream out = new FileOutputStream(file)) { - src.transferTo(out); - } catch (Exception e) { - // silence! - } - }).start(); - } - - public static void closeSilent(Closeable handle) { - try { - handle.close(); - } catch (Exception e) { - // silence! - } - } - - public static String readAll(final InputStream src) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - src.transferTo(out); - return out.toString(); - } - - public static String readAll(final InputStream src, String charset) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(); - src.transferTo(out); - return out.toString(charset); - } - - public static JsonGenerator createDefaultJsonGenerator() { - return new JsonGenerator.Options() - .excludeNulls() - .addConverter(new Converter() { - public boolean handles(Class type) { - return (File.class == type); - } - - public Object convert(Object value, String key) { - return ((File) value).getPath(); - } - }) - .build(); - } - - public static String toJsonPretty(Object value) { - return JsonOutput.prettyPrint(createDefaultJsonGenerator().toJson(value)); - } - - public static boolean isNullOrEmptyString(String value) { - return (value == null || value.length() == 0); - } - - public static Action wrapClosure(Closure closure) { - return x -> { - closure.setDelegate(x); - closure.setResolveStrategy(Closure.DELEGATE_FIRST); - closure.call(x); - }; - } - -} \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java b/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java @@ -0,0 +1,67 @@ +package org.implab.gradle.containers.cli; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.gradle.api.logging.Logger; + +public abstract class DockerTraits { + + public abstract Logger getLogger(); + + public abstract Optional getWorkingDir(); + + public abstract String getCliCmd(); + + boolean execute(ProcessBuilder builder, int code) throws IOException, InterruptedException { + var proc = builder.start(); + return proc.waitFor() == code; + } + + protected ProcessBuilder builder(String... args) { + var command = new ArrayList(args.length + 1); + command.add(getCliCmd()); + Arrays.stream(args).forEach(command::add); + + var builder = new ProcessBuilder(command); + + getWorkingDir().ifPresent(builder::directory); + return builder(); + } + + protected ProcessBuilder builder(List command) { + var builder = new ProcessBuilder(command); + + getWorkingDir().ifPresent(builder::directory); + return builder(); + } + + public void build(List args) { + + } + + public boolean imageExists(String imageId) throws InterruptedException, IOException { + getLogger().info("Check image {} exists", imageId); + + var builder = builder("image", "inspect", "--format", "image-exists", imageId); + + return execute(builder, 0); + } + + public boolean imageExists(File imageIdFile) { + if (imageIdFile.exists()) { + try { + var imageId = Files.readString(imageIdFile.toPath()); + return imageExists(imageId); + } catch (IOException | InterruptedException e) { + getLogger().error("Failed to read imageId {}: {}", imageIdFile, e); + return false; + } + } + return false; + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/cli/Utils.java b/container/src/main/java/org/implab/gradle/containers/cli/Utils.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/Utils.java @@ -0,0 +1,93 @@ +package org.implab.gradle.containers.cli; + +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.util.Scanner; + +import org.gradle.api.Action; + +import groovy.json.JsonGenerator; +import groovy.json.JsonOutput; +import groovy.json.JsonGenerator.Converter; +import groovy.lang.Closure; + +public final class Utils { + public static void redirectIO(final InputStream src, final Action consumer) { + new Thread(() -> { + try (Scanner sc = new Scanner(src)) { + while (sc.hasNextLine()) { + consumer.execute(sc.nextLine()); + } + } + }).start(); + } + + public static void redirectIO(final InputStream src, final File file) { + new Thread(() -> { + try (OutputStream out = new FileOutputStream(file)) { + src.transferTo(out); + } catch (Exception e) { + // silence! + } + }).start(); + } + + public static void closeSilent(Closeable handle) { + try { + handle.close(); + } catch (Exception e) { + // silence! + } + } + + public static String readAll(final InputStream src) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + src.transferTo(out); + return out.toString(); + } + + public static String readAll(final InputStream src, String charset) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + src.transferTo(out); + return out.toString(charset); + } + + public static JsonGenerator createDefaultJsonGenerator() { + return new JsonGenerator.Options() + .excludeNulls() + .addConverter(new Converter() { + public boolean handles(Class type) { + return (File.class == type); + } + + public Object convert(Object value, String key) { + return ((File) value).getPath(); + } + }) + .build(); + } + + public static String toJsonPretty(Object value) { + return JsonOutput.prettyPrint(createDefaultJsonGenerator().toJson(value)); + } + + public static boolean isNullOrEmptyString(String value) { + return (value == null || value.length() == 0); + } + + public static Action wrapClosure(Closure closure) { + return x -> { + closure.setDelegate(x); + closure.setResolveStrategy(Closure.DELEGATE_FIRST); + closure.call(x); + }; + } + +} \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/MapPropertyEntry.java b/container/src/main/java/org/implab/gradle/containers/dsl/MapPropertyEntry.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/dsl/MapPropertyEntry.java @@ -0,0 +1,35 @@ +package org.implab.gradle.containers.dsl; + +import java.util.concurrent.Callable; +import org.gradle.api.provider.MapProperty; +import org.gradle.api.provider.Provider; +import org.gradle.api.provider.ProviderFactory; + +public class MapPropertyEntry { + + private final MapProperty map; + + private final K key; + + private final ProviderFactory providerFactory; + + public MapPropertyEntry(MapProperty map, K key, ProviderFactory providerFactory) { + this.map = map; + this.key = key; + this.providerFactory = providerFactory; + } + + void put(Callable value) { + map.put(key, providerFactory.provider(value)); + } + + void put(V value) { + map.put(key, value); + } + + void put(Provider value) { + map.put(key, value); + } + + +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/BuildImage.java @@ -1,5 +1,9 @@ package org.implab.gradle.containers.tasks; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; @@ -8,19 +12,22 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import org.apache.tools.ant.taskdefs.UpToDate; import org.gradle.api.Action; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputDirectory; import org.gradle.api.tasks.OutputFile; import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.TaskAction; import org.implab.gradle.containers.ImageName; -import org.implab.gradle.containers.Utils; +import org.implab.gradle.containers.cli.Utils; +import org.implab.gradle.containers.dsl.MapPropertyEntry; import groovy.lang.Closure; @@ -39,6 +46,7 @@ public abstract class BuildImage extends public abstract ListProperty getExtraCommandArgs(); @Input + @org.gradle.api.tasks.Optional public abstract Property getBuildTarget(); @Input @@ -47,6 +55,13 @@ public abstract class BuildImage extends @OutputFile public abstract RegularFileProperty getImageIdFile(); + protected BuildImage() { + getOutputs().upToDateWhen(task -> getImageIdFile() + .map(RegularFile::getAsFile) + .map(docker()::imageExists) + .getOrElse(false)); + } + public void buildArgs(Action> spec) { getBuildArgs().putAll(provider(() -> { Map args = new HashMap<>(); @@ -59,6 +74,39 @@ public abstract class BuildImage extends buildArgs(Utils.wrapClosure(spec)); } + public MapPropertyEntry buildArg(String key) { + return new MapPropertyEntry(getBuildArgs(), key, getProviders()); + } + + @TaskAction + public void Run() throws Exception { + List args = new ArrayList<>(); + + args.addAll(List.of( + "-t", getImageName().get().toString(), + "--iidfile", getImageIdFile().getAsFile().get().toString())); + + getBuildArgs().get().forEach((k, v) -> { + args.add("--build-arg"); + args.add(String.format("%s=%s", k, v)); + }); + + // add --target if specified for multi-stage build + if (getBuildTarget().isPresent()) { + args.add("--target"); + args.add(getBuildTarget().get()); + } + + // add extra parameters + getExtraCommandArgs().getOrElse(Collections.emptyList()) + .forEach(args::add); + + args.add(getContextDirectory().getAsFile().get().toString()); + + docker().build(args); + } + + @Override protected Optional getSubCommand() { return Optional.of(BUILD_COMMAND); @@ -72,12 +120,10 @@ public abstract class BuildImage extends "-t", getImageName().get().toString(), "--iidfile", getImageIdFile().getAsFile().get().toString())); - if (imageBuildArgs.isPresent()) { - imageBuildArgs.get().forEach((k, v) -> { - args.add("--build-arg"); - args.add(String.format("%s=%s", k, v)); - }); - } + getBuildArgs().get().forEach((k, v) -> { + args.add("--build-arg"); + args.add(String.format("%s=%s", k, v)); + }); // add --target if specified for multi-stage build if (getBuildTarget().isPresent()) { diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java b/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/DockerCliTask.java @@ -7,7 +7,9 @@ import java.util.List; import java.util.Optional; import org.gradle.api.DefaultTask; +import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.logging.Logger; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; @@ -16,6 +18,7 @@ import org.gradle.api.tasks.TaskAction; import org.implab.gradle.containers.ContainerExtension; import org.implab.gradle.containers.ExecuteMixin; import org.implab.gradle.containers.PropertiesMixin; +import org.implab.gradle.containers.cli.DockerTraits; public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin, ExecuteMixin { @@ -68,7 +71,10 @@ public abstract class DockerCliTask exte @Override @Internal public Optional getWorkingDir() { - return Optional.ofNullable(workingDir.get().getAsFile()); + return workingDir + .map(Directory::getAsFile) + .map(Optional::of) + .getOrElse(Optional.empty()); } protected void setWorkingDir(Provider workingDir) { @@ -78,4 +84,27 @@ public abstract class DockerCliTask exte protected void setWorkingDir(File workingDir) { this.workingDir.set(workingDir); } + + protected DockerTraits docker() { + return new TaskDockerTraits(); + } + + class TaskDockerTraits extends DockerTraits { + + @Override + public Logger getLogger() { + return DockerCliTask.this.getLogger(); + } + + @Override + public Optional getWorkingDir() { + return DockerCliTask.this.getWorkingDir(); + } + + @Override + public String getCliCmd() { + return cliCmd.get(); + } + + } }