# HG changeset patch # User cin # Date 2023-11-19 22:22:07 # Node ID 902127e66316cc3b53dbedf7ace0c0725df0c86f # Parent 04caf9830434485e115ff9b872e4f213f57d5c1c Working version of containers plugin, extracted container-base plugin for basic types and configuration diff --git a/container/build.gradle b/container/build.gradle --- a/container/build.gradle +++ b/container/build.gradle @@ -5,6 +5,16 @@ plugins { id "ivy-publish" } +dependencies { + implementation "com.fasterxml.jackson.core:jackson-core:2.13.5", + "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.5" +} + +java { + targetCompatibility = 11 + sourceCompatibility = 11 +} + repositories { mavenCentral() @@ -16,11 +26,18 @@ gradlePlugin { plugins { containerPlugin { id = 'org.implab.gradle-container' - displayName = "Container building plugin" + displayName = "Provdes convetional configuration to build a container image" description = 'Build and publish container images with docker or podman. Simple wrapper around cli.' implementationClass = 'org.implab.gradle.containers.ContainerPlugin' tags.set(['containers', 'image', 'docker', 'podman']) } + containerBasePlugin { + id = 'org.implab.gradle-container-base' + displayName = "Provides tasks to build manipulate container images" + description = 'Build and publish container images with docker or podman. Simple wrapper around cli.' + implementationClass = 'org.implab.gradle.containers.ContainerBasePlugin' + tags.set(['containers', 'image', 'docker', 'podman']) + } } } diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java b/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/ContainerBasePlugin.java @@ -0,0 +1,49 @@ +package org.implab.gradle.containers; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.plugins.ExtraPropertiesExtension; +import org.implab.gradle.containers.tasks.BuildImage; +import org.implab.gradle.containers.tasks.PushImage; +import org.implab.gradle.containers.tasks.RunImage; +import org.implab.gradle.containers.tasks.SaveImage; +import org.implab.gradle.containers.tasks.TagImage; + +public class ContainerBasePlugin implements Plugin { + public static final String CONTAINER_EXTENSION_NAME = "container"; + + ContainerExtension containerExtension; + + ContainerExtension getContainerExtension() { + if (containerExtension == null) + throw new IllegalStateException(); + return containerExtension; + } + + void exportClasses(Project project, Class... classes) { + ExtraPropertiesExtension extras = project.getExtensions().getExtraProperties(); + for (var clazz : classes) + extras.set(clazz.getSimpleName(), clazz); + } + + @Override + public void apply(Project project) { + + containerExtension = project.getObjects().newInstance(ContainerExtension.class); + + containerExtension.getImageAuthority() + .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority"))); + + containerExtension.getImageGroup() + .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup"))); + + project.getExtensions().add(CONTAINER_EXTENSION_NAME, containerExtension); + + exportClasses( + project, + BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunImage.class); + + + } + +} diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java b/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java --- a/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java +++ b/container/src/main/java/org/implab/gradle/containers/ContainerExtension.java @@ -1,156 +1,85 @@ package org.implab.gradle.containers; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; import java.util.Optional; +import javax.inject.Inject; + import org.gradle.api.Project; -import org.gradle.api.file.Directory; import org.gradle.api.file.DirectoryProperty; import org.gradle.api.file.ProjectLayout; -import org.gradle.api.file.RegularFile; import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; - -public class ContainerExtension { - - private final Property cliCmd; +import org.implab.gradle.containers.cli.ImageName; +import org.implab.gradle.containers.cli.ImageRef; +import org.implab.gradle.containers.cli.Utils; - private final Property imageAuthority; - - private final Property imageGroup; +public abstract class ContainerExtension { - private final Property imageShortName; - - private final Property imageTag; + public abstract Property getCliCmd(); - private final Property imageName; + public abstract DirectoryProperty getContextDirectory(); - private final DirectoryProperty contextDir; - - private final RegularFileProperty imageIdFile; + public abstract RegularFileProperty getImageIdFile(); - public ContainerExtension(ObjectFactory factory, ProjectLayout layout, Project project) { - contextDir = factory.directoryProperty(); - contextDir.convention(layout.getBuildDirectory().dir("context")); - - imageIdFile = factory.fileProperty(); - imageIdFile.convention(layout.getBuildDirectory().file("imageId")); - - cliCmd = factory.property(String.class); - cliCmd.set("docker"); + /** + * Specifies the name of the registry where the image is located + * {@code registry.my-company.org} + */ + public abstract Property getImageAuthority(); - imageAuthority = factory.property(String.class); - imageGroup = factory.property(String.class); - imageShortName = factory.property(String.class); - - imageShortName.convention(project.getName()); + /** + * Specified the path of the image like {@code my-company} + */ + public abstract Property getImageGroup(); - imageTag = factory.property(String.class); - imageTag.set(project - .provider(() -> Optional.ofNullable(project.getVersion()).map(v -> v.toString()).orElse("latest"))); - - Provider imageRepository = imageGroup.map(g -> g + "/" + imageShortName.get()).orElse(imageShortName); + public abstract Property getImageName(); - imageName = factory.property(ImageName.class); - imageName.convention(project.provider( - () -> new ImageName().authority(imageAuthority.get()).name(imageRepository.get()).tag(imageTag.get()))); - } - - public Property getCliCmd() { - return cliCmd; - } + /** + * Specifies local image part like {@code httpd} + */ + public abstract Property getImageLocalName(); - public void setCliCmd(String cliCmd) { - this.cliCmd.set(cliCmd); - } - - public DirectoryProperty getContextDirectory() { - return contextDir; - } - - public void setContextDirectory(Directory dir) { - contextDir.set(dir); + /** + * This property is deprecated use imageLocalName + */ + @Deprecated + public Property getImageShortName() { + return getImageLocalName(); } - public void setContextDirectory(Provider dir) { - contextDir.set(dir); - } - - public RegularFileProperty getImageIdFile() { - return imageIdFile; - } - - public void setImageIdFile(RegularFile file) { - imageIdFile.set(file); - } - - public void setImageIdFile(Provider file) { - imageIdFile.set(file); - } + public abstract Property getImageTag(); - public Property getImageAuthority() { - return imageAuthority; - } - - public void setImageAuthority(String value) { - imageAuthority.set(value); - } + @Inject + public ContainerExtension(ProjectLayout layout, Project project) { + getContextDirectory().convention(layout.getBuildDirectory().dir("context")); - public void setImageAuthority(Provider value) { - imageAuthority.set(value); - } + getImageIdFile().convention(layout.getBuildDirectory().file("iid.json")); - public Property getImageGroup() { - return imageGroup; - } - - public void setImageGroup(String value) { - imageGroup.set(value); - } + getCliCmd().set("docker"); - public void setImageGroup(Provider value) { - imageGroup.set(value); - } - - public Property getImageName() { - return imageName; - } + getImageLocalName().convention(project.getName()); - public void setImageName(ImageName name) { - imageName.set(name); - } - - public void setImageName(Provider name) { - imageName.set(name); - } + getImageTag().set(project + .provider(() -> Optional.ofNullable(project.getVersion()).map(v -> v.toString()).orElse("latest"))); - public Property getImageShortName() { - return imageShortName; - } - - public void setImageShortName(String name) { - imageShortName.set(name); - } + Provider imageRepository = getImageGroup().map(g -> g + "/" + getImageLocalName().get()) + .orElse(getImageLocalName()); - public void setImageShortName(Provider name) { - imageShortName.set(name); - } - - public Property getImageTag() { - return imageTag; - } - - public void setImageTag(String tag) { - imageTag.set(tag); - } - - public void setImageTag(Provider tag) { - imageTag.set(tag); + getImageName().convention(project.provider( + () -> new ImageName().authority(getImageAuthority().get()).name(imageRepository.get()) + .tag(getImageTag().get()))); } public ImageName createImageName() { return new ImageName(); } + public ImageRef readImageRef(File file) throws FileNotFoundException, IOException { + return Utils.readImageRef(file); + } + } \ No newline at end of file diff --git a/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java b/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java --- a/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java +++ b/container/src/main/java/org/implab/gradle/containers/ContainerPlugin.java @@ -1,52 +1,36 @@ package org.implab.gradle.containers; +import org.gradle.api.GradleException; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ProjectLayout; -import org.gradle.api.model.ObjectFactory; -import org.gradle.api.plugins.ExtraPropertiesExtension; import org.gradle.api.tasks.Copy; import org.gradle.api.tasks.Delete; import org.gradle.api.tasks.TaskProvider; +import org.implab.gradle.containers.cli.ImageName; import org.implab.gradle.containers.tasks.BuildImage; import org.implab.gradle.containers.tasks.PushImage; -import org.implab.gradle.containers.tasks.RunImage; import org.implab.gradle.containers.tasks.SaveImage; -import org.implab.gradle.containers.tasks.TagImage; public class ContainerPlugin implements Plugin { public static final String BUILD_GROUP = "build"; - public static final String CONTAINER_EXTENSION_NAME = "container"; - public static final String ARCHIVE_CONFIGURATION = "archive"; - private ContainerExtension containerExtension; - - void exportClasses(Project project, Class... classes) { - ExtraPropertiesExtension extras = project.getExtensions().getExtraProperties(); - for (var clazz : classes) - extras.set(clazz.getSimpleName(), clazz); - } - + ContainerExtension containerExtension; + public void apply(Project project) { - ObjectFactory factory = project.getObjects(); ProjectLayout layout = project.getLayout(); - containerExtension = new ContainerExtension(factory, layout, project); - containerExtension.getImageAuthority() - .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority"))); + project.getPluginManager().apply(ContainerBasePlugin.class); - containerExtension.getImageGroup() - .convention(project.provider(() -> (String) project.getProperties().get("imagesGroup"))); + var basePlugin = project.getPlugins().findPlugin(ContainerBasePlugin.class); + if (basePlugin == null) + throw new GradleException("The container-base plugin fails to be applied"); - project.getExtensions().add(CONTAINER_EXTENSION_NAME, containerExtension); - - exportClasses( - project, - BuildImage.class, PushImage.class, SaveImage.class, TagImage.class, RunImage.class); + var containerExtension = basePlugin.getContainerExtension(); project.getConfigurations().create(Dependency.DEFAULT_CONFIGURATION, c -> { c.setCanBeConsumed(true); @@ -85,17 +69,14 @@ public class ContainerPlugin implements project.getTasks().register("pushImage", PushImage.class, t -> { t.dependsOn(buildImageTask); - t.getImageName().set(buildImageTask.flatMap(b -> b.getImageName())); + t.getImageName().set(buildImageTask.flatMap(BuildImage::getImageName)); }); project.getTasks().register("saveImage", SaveImage.class, t -> { t.dependsOn(buildImageTask); - t.getImage().set(buildImageTask.flatMap(b -> b.getImageName())); + t.getExportImages().add(buildImageTask.flatMap(BuildImage::getImageName).map(ImageName::toString)); }); - project.getArtifacts().add(Dependency.DEFAULT_CONFIGURATION, buildImageTask.flatMap(x -> x.getImageIdFile()), - t -> { - t.builtBy(buildImageTask); - }); + project.getArtifacts().add(Dependency.DEFAULT_CONFIGURATION, buildImageTask); } } diff --git a/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java b/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java deleted file mode 100644 --- a/container/src/main/java/org/implab/gradle/containers/ExecuteMixin.java +++ /dev/null @@ -1,66 +0,0 @@ -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 IOException, InterruptedException { - final Logger log = getLogger(); - - List command = new ArrayList<>(); - - preparingCommand(command); - - ProcessBuilder builder = new ProcessBuilder(command); - getWorkingDir().ifPresent(workingDir -> builder.directory(workingDir)); - - log.info("Starting: {}", builder.command()); - - Process p = builder.start(); - - processStarted(p); - - if (log.isErrorEnabled()) { - Utils.redirectIO(p.getErrorStream(), log::error); - } - - int code = p.waitFor(); - 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); - - - /** - * Invoked after the process is started, can be used to handle the process - * output. - * - * Default implementation redirects output to the logger as INFO messages. - * - * @param p The current process. - */ - - default void processStarted(Process p) { - final Logger log = getLogger(); - Utils.redirectIO(p.getInputStream(), log::info); - } -} 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 --- a/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/DockerTraits.java @@ -7,56 +7,131 @@ 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 final String BUILD_COMMAND = "build"; + public final String PUSH_COMMAND = "push"; + public final String RUN_COMMAND = "run"; + public final String SAVE_COMMAND = "save"; + public final String INSPECT_COMMAND = "inspect"; + public final String IMAGE_COMMAND = "image"; + public final String TAG_COMMAND = "tag"; + 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(); + Process startProcess(ProcessBuilder builder) throws IOException { + getLogger().info("Starting: {}", builder.command()); + return builder.start(); + } + + protected boolean checkRetCode(Process proc, int code) throws InterruptedException { + if (getLogger().isInfoEnabled()) { + Utils.redirectIO(proc.getInputStream(), getLogger()::info); + Utils.redirectIO(proc.getErrorStream(), getLogger()::info); + } + return proc.waitFor() == code; } + protected void complete(Process proc) throws InterruptedException, IOException { + if (getLogger().isInfoEnabled()) + Utils.redirectIO(proc.getInputStream(), getLogger()::info); + + if (getLogger().isErrorEnabled()) + Utils.redirectIO(proc.getErrorStream(), getLogger()::error); + + var code = proc.waitFor(); + if (code != 0) { + getLogger().error("The process exited with code {}", code); + throw new IOException("The process exited with error code " + code); + } + } + protected ProcessBuilder builder(String... args) { - var command = new ArrayList(args.length + 1); + var argsList = new ArrayList(args.length + 1); + Arrays.stream(args).forEach(argsList::add); + + return builder(argsList); + } + + protected ProcessBuilder builder(List args) { + var command = new ArrayList(args.size() + 1); + command.add(getCliCmd()); - Arrays.stream(args).forEach(command::add); + args.forEach(command::add); var builder = new ProcessBuilder(command); getWorkingDir().ifPresent(builder::directory); - return builder(); + return builder; + } + + public void buildImage(String imageName, File contextDirectory, List options) + throws IOException, InterruptedException { + var args = new ArrayList(); + args.add(BUILD_COMMAND); + args.addAll(options); + args.add("-t"); + args.add(imageName); + args.add(contextDirectory.getAbsolutePath()); + complete(startProcess(builder(args))); + } + + public void pushImage(String image, List options) throws InterruptedException, IOException { + var args = new ArrayList(); + args.add(PUSH_COMMAND); + args.addAll(options); + args.add(image); + complete(startProcess(builder(args))); } - protected ProcessBuilder builder(List command) { - var builder = new ProcessBuilder(command); - - getWorkingDir().ifPresent(builder::directory); - return builder(); + public void runImage(String image, List options, List command) + throws InterruptedException, IOException { + var args = new ArrayList(); + args.add(RUN_COMMAND); + args.addAll(options); + args.add(image); + args.addAll(command); + complete(startProcess(builder(args))); } - public void build(List args) { - + public void saveImage(List images, File output) throws InterruptedException, IOException { + if (output.exists()) + output.delete(); + + var args = new ArrayList(); + args.add(SAVE_COMMAND); + args.add("-o"); + args.add(output.getAbsolutePath()); + images.forEach(args::add); + + complete(startProcess(builder(args))); + } + + public void tagImage(String source, String target) throws InterruptedException, IOException { + complete(startProcess(builder(TAG_COMMAND, source, target))); } 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); + return checkRetCode( + startProcess(builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId)), + 0); } public boolean imageExists(File imageIdFile) { if (imageIdFile.exists()) { try { - var imageId = Files.readString(imageIdFile.toPath()); - return imageExists(imageId); + var imageId = Utils.readImageRef(imageIdFile); + return imageExists(imageId.getId()); } catch (IOException | InterruptedException e) { getLogger().error("Failed to read imageId {}: {}", imageIdFile, e); return false; diff --git a/container/src/main/java/org/implab/gradle/containers/ImageName.java b/container/src/main/java/org/implab/gradle/containers/cli/ImageName.java rename from container/src/main/java/org/implab/gradle/containers/ImageName.java rename to container/src/main/java/org/implab/gradle/containers/cli/ImageName.java --- a/container/src/main/java/org/implab/gradle/containers/ImageName.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/ImageName.java @@ -1,10 +1,13 @@ -package org.implab.gradle.containers; +package org.implab.gradle.containers.cli; import java.io.Serializable; import java.util.Optional; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + public class ImageName implements Serializable { - + private static final long serialVersionUID = -1990105923537254552L; final String authority; @@ -19,7 +22,9 @@ public class ImageName implements Serial tag = null; } - private ImageName(String authority, String name, String tag) { + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + private ImageName(@JsonProperty("authority") String authority, @JsonProperty("name") String name, + @JsonProperty("tag") String tag) { this.authority = authority; this.name = name; this.tag = tag; @@ -59,7 +64,7 @@ public class ImageName implements Serial Optional.ofNullable(authority).ifPresent(s -> sb.append(s).append("/")); Optional.ofNullable(name).ifPresent(s -> sb.append(s)); - Optional.ofNullable(tag).ifPresent(s-> sb.append(":").append(s)); + Optional.ofNullable(tag).ifPresent(s -> sb.append(":").append(s)); return sb.toString(); } diff --git a/container/src/main/java/org/implab/gradle/containers/cli/ImageRef.java b/container/src/main/java/org/implab/gradle/containers/cli/ImageRef.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/cli/ImageRef.java @@ -0,0 +1,38 @@ +package org.implab.gradle.containers.cli; + +import java.io.Serializable; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class ImageRef implements Serializable { + + private static final long serialVersionUID = 2023111901L; + + final String tag; + + final String id; + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public ImageRef(@JsonProperty("tag") String tag, @JsonProperty("id") String id) { + this.id = id; + this.tag = tag; + } + + public String getId() { + return id; + } + + public ImageRef withId(String id) { + return new ImageRef(tag, id); + } + + public Optional getTag() { + return Optional.ofNullable(tag); + } + + public ImageRef withTag(String tag) { + return new ImageRef(tag, id); + } +} 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 --- a/container/src/main/java/org/implab/gradle/containers/cli/Utils.java +++ b/container/src/main/java/org/implab/gradle/containers/cli/Utils.java @@ -3,15 +3,25 @@ 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.FileNotFoundException; import java.io.FileOutputStream; +import java.io.FileReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.nio.file.Files; +import java.io.Reader; import java.util.Scanner; +import java.util.stream.StreamSupport; +import java.nio.file.Files; +import java.util.List; +import org.gradle.api.Action; +import org.gradle.internal.impldep.org.bouncycastle.util.Iterable; -import org.gradle.api.Action; +import com.fasterxml.jackson.core.exc.StreamWriteException; +import com.fasterxml.jackson.databind.DatabindException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import groovy.json.JsonGenerator; import groovy.json.JsonOutput; @@ -53,6 +63,42 @@ public final class Utils { return out.toString(); } + public static T readJson(final Reader reader, Class type) throws IOException { + ObjectMapper objectMapper = new ObjectMapper() + .registerModule(new Jdk8Module()); + return objectMapper.readValue(reader, type); + } + + public static void writeJson(final File file, Object value) + throws StreamWriteException, DatabindException, IOException { + ObjectMapper objectMapper = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT) + .registerModule(new Jdk8Module()); + objectMapper.writeValue(file, value); + } + + public static ImageRef readImageRef(final File file) throws FileNotFoundException, IOException { + try (var reader = new FileReader(file)) { + return readJson(reader, ImageRef.class); + } + } + + public static String readAll(final File src) throws IOException { + return Files.readString(src.toPath()); + } + + public static List readAll(final Iterable files) { + return StreamSupport.stream(files.spliterator(), false) + .map(file -> { + try { + return Utils.readAll(file); + } catch (IOException e) { + throw new RuntimeException(e); + } + }) + .toList(); + } + public static String readAll(final InputStream src, String charset) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); src.transferTo(out); @@ -85,7 +131,7 @@ public final class Utils { public static Action wrapClosure(Closure closure) { return x -> { closure.setDelegate(x); - closure.setResolveStrategy(Closure.DELEGATE_FIRST); + closure.setResolveStrategy(Closure.OWNER_FIRST); closure.call(x); }; } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/MountSpec.java b/container/src/main/java/org/implab/gradle/containers/dsl/MountSpec.java rename from container/src/main/java/org/implab/gradle/containers/tasks/MountSpec.java rename to container/src/main/java/org/implab/gradle/containers/dsl/MountSpec.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/MountSpec.java +++ b/container/src/main/java/org/implab/gradle/containers/dsl/MountSpec.java @@ -1,4 +1,4 @@ -package org.implab.gradle.containers.tasks; +package org.implab.gradle.containers.dsl; public class MountSpec { diff --git a/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java b/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java new file mode 100644 --- /dev/null +++ b/container/src/main/java/org/implab/gradle/containers/dsl/OptionsMixin.java @@ -0,0 +1,39 @@ +package org.implab.gradle.containers.dsl; + +import java.util.concurrent.Callable; + +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.Input; +import org.implab.gradle.containers.PropertiesMixin; + +import groovy.lang.Closure; + +public interface OptionsMixin extends PropertiesMixin { + + @Input + ListProperty getOptions(); + + default void option(String opt) { + getOptions().add(opt); + } + + default void option(Callable optionProvider) { + getOptions().add(provider(optionProvider)); + } + + default void option(Closure optionProvider) { + getOptions().add(provider(optionProvider)); + } + + default void options(String ...opts) { + getOptions().addAll(opts); + } + + default void options(Callable> optionsProvider) { + getOptions().addAll(provider(optionsProvider)); + } + + default void options(Closure> optionsProvider) { + getOptions().addAll(provider(optionsProvider)); + } +} diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/VolumeSpec.java b/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java rename from container/src/main/java/org/implab/gradle/containers/tasks/VolumeSpec.java rename to container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/VolumeSpec.java +++ b/container/src/main/java/org/implab/gradle/containers/dsl/VolumeSpec.java @@ -1,4 +1,4 @@ -package org.implab.gradle.containers.tasks; +package org.implab.gradle.containers.dsl; import java.util.ArrayList; 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,40 +1,36 @@ 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; import java.util.HashMap; 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.Directory; 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.Internal; +import org.gradle.api.tasks.Optional; 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 java.io.File; + +import org.implab.gradle.containers.cli.ImageName; +import org.implab.gradle.containers.cli.ImageRef; import org.implab.gradle.containers.cli.Utils; import org.implab.gradle.containers.dsl.MapPropertyEntry; +import org.implab.gradle.containers.dsl.OptionsMixin; import groovy.lang.Closure; -public abstract class BuildImage extends DockerCliTask { - - public final String BUILD_COMMAND = "build"; - +public abstract class BuildImage extends DockerCliTask implements OptionsMixin { @InputDirectory @SkipWhenEmpty public abstract DirectoryProperty getContextDirectory(); @@ -43,19 +39,21 @@ public abstract class BuildImage extends public abstract MapProperty getBuildArgs(); @Input - public abstract ListProperty getExtraCommandArgs(); - - @Input - @org.gradle.api.tasks.Optional + @Optional public abstract Property getBuildTarget(); @Input public abstract Property getImageName(); - @OutputFile + @Internal public abstract RegularFileProperty getImageIdFile(); - protected BuildImage() { + @OutputFile + public Provider getImageIdFileOutput() { + return getImageIdFile().map(RegularFile::getAsFile); + } + + public BuildImage() { getOutputs().upToDateWhen(task -> getImageIdFile() .map(RegularFile::getAsFile) .map(docker()::imageExists) @@ -66,6 +64,7 @@ public abstract class BuildImage extends getBuildArgs().putAll(provider(() -> { Map args = new HashMap<>(); spec.execute(args); + getLogger().info("add buildArgs {}", args); return args; })); } @@ -79,12 +78,14 @@ public abstract class BuildImage extends } @TaskAction - public void Run() throws Exception { + public void run() throws Exception { List args = new ArrayList<>(); - args.addAll(List.of( - "-t", getImageName().get().toString(), - "--iidfile", getImageIdFile().getAsFile().get().toString())); + // create a temp file to store image id + var iidFile = new File(this.getTemporaryDir(), "iid"); + + // specify where to write image id + args.addAll(List.of("--iidfile", iidFile.getAbsolutePath())); getBuildArgs().get().forEach((k, v) -> { args.add("--build-arg"); @@ -98,45 +99,18 @@ public abstract class BuildImage extends } // add extra parameters - getExtraCommandArgs().getOrElse(Collections.emptyList()) - .forEach(args::add); - - args.add(getContextDirectory().getAsFile().get().toString()); - - docker().build(args); - } - + getOptions().get().forEach(args::add); - @Override - protected Optional getSubCommand() { - return Optional.of(BUILD_COMMAND); - } - - @Override - protected Collection getSubCommandArguments() { - List args = new ArrayList<>(); + var imageTag = getImageName().map(ImageName::toString).get(); - 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)); - }); + // build image + docker().buildImage( + imageTag, + getContextDirectory().map(Directory::getAsFile).get(), + args); - // 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()); - - return args; + // read the built image id and store image ref metadata + var imageRef = new ImageRef(imageTag, Utils.readAll(iidFile)); + Utils.writeJson(getImageIdFileOutput().get(), imageRef); } } 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 @@ -1,9 +1,6 @@ package org.implab.gradle.containers.tasks; import java.io.File; -import java.util.Collection; -import java.util.Collections; -import java.util.List; import java.util.Optional; import org.gradle.api.DefaultTask; @@ -11,80 +8,31 @@ 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; import org.gradle.api.tasks.Internal; -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 { - - private final Property cliCmd; - - private final DirectoryProperty workingDir; - - public DockerCliTask() { - cliCmd = property(String.class) - .convention(getContainerExtension().getCliCmd()); - - workingDir = directoryProperty(); - } - - @Internal - protected Optional getSubCommand() { - return Optional.empty(); - } - - @Internal - protected Collection getSubCommandArguments() { - return Collections.emptyList(); - } +public abstract class DockerCliTask extends DefaultTask implements PropertiesMixin { @Input - public Property getCliCmd() { - return cliCmd; - } + public abstract Property getCliCmd(); - public void setCliCmd(String cliCmd) { - this.cliCmd.set(cliCmd); - } + @Input + @org.gradle.api.tasks.Optional + public abstract DirectoryProperty getWorkingDirectory(); @Internal protected ContainerExtension getContainerExtension() { return getProject().getExtensions().getByType(ContainerExtension.class); } - @TaskAction - public void Run() throws Exception { - Execute(); - } - - public void preparingCommand(List commandLine) { - commandLine.add(cliCmd.get()); - getSubCommand().ifPresent(commandLine::add); - getSubCommandArguments().forEach(commandLine::add); - } + public DockerCliTask() { + getCliCmd().convention(getContainerExtension().getCliCmd()); - @Override - @Internal - public Optional getWorkingDir() { - return workingDir - .map(Directory::getAsFile) - .map(Optional::of) - .getOrElse(Optional.empty()); } - - protected void setWorkingDir(Provider workingDir) { - this.workingDir.fileProvider(workingDir); - } - - protected void setWorkingDir(File workingDir) { - this.workingDir.set(workingDir); - } - + protected DockerTraits docker() { return new TaskDockerTraits(); } @@ -98,12 +46,15 @@ public abstract class DockerCliTask exte @Override public Optional getWorkingDir() { - return DockerCliTask.this.getWorkingDir(); + return getWorkingDirectory() + .map(Directory::getAsFile) + .map(Optional::of) + .getOrElse(Optional.empty()); } @Override public String getCliCmd() { - return cliCmd.get(); + return DockerCliTask.this.getCliCmd().get(); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/PushImage.java @@ -1,19 +1,16 @@ package org.implab.gradle.containers.tasks; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; +import java.io.IOException; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; -import org.implab.gradle.containers.ImageName; +import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.ImageName; +import org.implab.gradle.containers.dsl.OptionsMixin; -public abstract class PushImage extends DockerCliTask { - - public final String PUSH_COMMAND = "push"; +public abstract class PushImage extends DockerCliTask implements OptionsMixin { @Input public abstract Property getImageName(); @@ -26,23 +23,11 @@ public abstract class PushImage extends @Optional public abstract ListProperty getOptions(); - public PushImage option(Callable provider) { - getOptions().add(getProject().provider(provider)); - return this; - } + @TaskAction + public void run() throws InterruptedException, IOException { - @Override - protected java.util.Optional getSubCommand() { - return java.util.Optional.of(PUSH_COMMAND); - } - - @Override - protected Collection getSubCommandArguments() { - List args = new ArrayList<>(); - - args.addAll(getOptions().get()); - args.add(getImageName().get().toString()); - - return args; + docker().pushImage( + getImageName().get().toString(), + getOptions().get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/RunImage.java @@ -1,23 +1,16 @@ package org.implab.gradle.containers.tasks; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - +import java.io.IOException; import org.gradle.api.Action; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Optional; -import org.implab.gradle.containers.ImageName; - -public abstract class RunImage extends DockerCliTask { +import org.implab.gradle.containers.cli.ImageName; +import org.implab.gradle.containers.dsl.OptionsMixin; +import org.implab.gradle.containers.dsl.VolumeSpec; - public final String RUN_COMMAND = "run"; - - @Input - @Optional - public abstract ListProperty getOptions(); +public abstract class RunImage extends DockerCliTask implements OptionsMixin { @Input public abstract Property getImageName(); @@ -25,22 +18,7 @@ public abstract class RunImage extends D @Input @Optional public abstract ListProperty getCommandLine(); - - @Override - protected java.util.Optional getSubCommand() { - return java.util.Optional.of(RUN_COMMAND); - } - - @Override - protected Collection getSubCommandArguments() { - List args = new ArrayList(); - - args.addAll(getOptions().get()); - args.add(getImageName().get().toString()); - args.addAll(getCommandLine().get()); - - return args; - } + public void volume(Action configure) { getOptions().add("-v"); @@ -51,4 +29,15 @@ public abstract class RunImage extends D })); } + void commandLine(String ...args) { + getCommandLine().addAll(args); + } + + public void run() throws InterruptedException, IOException { + docker().runImage( + getImageName().get().toString(), + getOptions().get(), + getCommandLine().get()); + } + } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/SaveImage.java @@ -1,25 +1,27 @@ package org.implab.gradle.containers.tasks; +import java.io.File; +import java.io.IOException; import java.util.ArrayList; -import java.util.Collection; -import java.util.List; import java.util.Optional; import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFile; +import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.OutputFile; +import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskExecutionException; -import org.implab.gradle.containers.ImageName; +import org.implab.gradle.containers.cli.Utils; public abstract class SaveImage extends DockerCliTask { - public final String SAVE_COMMAND = "save"; @Input - public abstract Property getImage(); + public abstract ListProperty getExportImages(); @OutputFile public Provider getArchiveFile() { @@ -44,6 +46,29 @@ public abstract class SaveImage extends @Internal public abstract Property getArchiveExtension(); + String readImageRefTag(File file) throws Exception { + getLogger().info("Reading image ref from {}", file); + var imageRef = Utils.readImageRef(file); + return imageRef.getTag().orElseThrow(() -> new Exception("The image tag is required to save image")); + } + + public void imageRef(File file) { + getExportImages().add(provider(() -> { + return readImageRefTag(file); + })); + } + + public void imageRefs(FileCollection files) { + dependsOn(files); + getExportImages().addAll(provider(() -> { + var tags = new ArrayList(); + for (File file : files) { + tags.add(readImageRefTag(file)); + } + return tags; + })); + } + public SaveImage() { getArchiveFileName().convention(provider(this::conventionalArchiveFileName)); getArchiveBaseName().convention(provider(this::conventionalArchiveBaseName)); @@ -77,16 +102,10 @@ public abstract class SaveImage extends return String.join("-", parts) + "." + getArchiveExtension().get(); } - @Override - protected Optional getSubCommand() { - return Optional.of(SAVE_COMMAND); - } - - @Override - protected Collection getSubCommandArguments() { - return List.of( - getImage().get().toString(), - getArchiveFile().get().getAsFile().toString() - ); + @TaskAction + public void run() throws InterruptedException, IOException { + docker().saveImage( + getExportImages().get(), + getArchiveFile().map(RegularFile::getAsFile).get()); } } diff --git a/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java b/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java --- a/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java +++ b/container/src/main/java/org/implab/gradle/containers/tasks/TagImage.java @@ -1,51 +1,22 @@ package org.implab.gradle.containers.tasks; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.Callable; - -import org.gradle.api.provider.ListProperty; +import java.io.IOException; import org.gradle.api.provider.Property; import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.Optional; -import org.implab.gradle.containers.ImageName; +import org.gradle.api.tasks.TaskAction; +import org.implab.gradle.containers.cli.ImageName; public abstract class TagImage extends DockerCliTask { - public final String TAG_COMMAND = "tag"; - @Input public abstract Property getSrcImage(); @Input public abstract Property getDestImage(); - @Input - @Optional - public abstract Property getTransport(); - - @Input - @Optional - public abstract ListProperty getOptions(); - - public TagImage option(Callable provider) { - getOptions().add(getProject().provider(provider)); - return this; - } - - @Override - protected java.util.Optional getSubCommand() { - return java.util.Optional.of(TAG_COMMAND); - } - - @Override - protected Collection getSubCommandArguments() { - List args = new ArrayList<>(); - - args.addAll(getOptions().get()); - args.add(getSrcImage().get().toString()); - args.add(getDestImage().get().toString()); - - return args; + @TaskAction + public void run() throws InterruptedException, IOException { + docker().tagImage( + getSrcImage().map(ImageName::toString).get(), + getDestImage().map(ImageName::toString).get()); } }