##// END OF EJS Templates
Implemented compose
cin -
r12:c7d470187afa v1.2.0 default
parent child
Show More
@@ -0,0 +1,17
1 package org.implab.gradle.containers;
2
3 import org.gradle.api.provider.Property;
4 import org.gradle.api.provider.SetProperty;
5
6 public abstract class ComposeExtension {
7 public final String COMPOSE_FILE = "compose.yaml";
8
9 public abstract SetProperty<String> getProfiles();
10
11 public abstract Property<String> getComposeFileName();
12
13 public ComposeExtension() {
14 getComposeFileName().convention(COMPOSE_FILE);
15 }
16
17 }
@@ -0,0 +1,159
1 package org.implab.gradle.containers;
2
3 import java.io.IOException;
4 import java.util.HashMap;
5 import java.util.Map;
6
7 import org.gradle.api.DefaultTask;
8 import org.gradle.api.Plugin;
9 import org.gradle.api.Project;
10 import org.gradle.api.artifacts.Configuration;
11 import org.gradle.api.artifacts.Configuration.State;
12 import org.gradle.api.logging.Logger;
13 import org.gradle.api.tasks.Copy;
14 import org.gradle.api.tasks.Delete;
15 import org.implab.gradle.containers.cli.Utils;
16 import org.implab.gradle.containers.tasks.ComposeRm;
17 import org.implab.gradle.containers.tasks.ComposeStop;
18 import org.implab.gradle.containers.tasks.ComposeUp;
19 import org.implab.gradle.containers.tasks.WriteEnv;
20
21 public abstract class ComposePlugin implements Plugin<Project>, ProjectMixin {
22 public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages";
23
24 public final String COMPOSE_EXTENSION = "compose";
25
26 public final String COMPOSE_UP_TASK = "up";
27
28 public final String COMPOSE_STOP_TASK = "stop";
29
30 public final String COMPOSE_RM_TASK = "rm";
31
32 public final String CLEAN_TASK = "clean";
33
34 public final String BUILD_TASK = "build";
35
36 public final String PROCESS_RESOURCES_TASK = "processResources";
37
38 public final String WRITE_ENV_TASK = "writeEnv";
39
40 public final String COMPOSE_VAR = "composeVar";
41
42 public final String ENV_FILE_NAME = ".env";
43
44 public Logger getLogger() {
45 return getProject().getLogger();
46 }
47
48 @Override
49 public void apply(Project project) {
50 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
51
52 // basic configuration, register extension
53 var basePlugin = plugin(ContainerBasePlugin.class);
54 var containerExtension = basePlugin.getContainerExtension();
55
56 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
57
58 var composeFile = containerExtension.getContextDirectory()
59 .file(composeExtension.getComposeFileName());
60 var composeProfiles = composeExtension.getProfiles();
61
62 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
63 t.delete(containerExtension.getContextDirectory());
64 });
65
66 // copy task from src/main
67 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
68 t.mustRunAfter(cleanTask);
69 t.from(projectDirectory().dir("src/main"));
70 t.into(containerExtension.getContextDirectory());
71 });
72
73 // write .env
74 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
75 t.dependsOn(processResources, containerImages);
76 t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME));
77
78 t.getEnvironment().putAll(containerImages.map(this::extractComposeEnv));
79
80 });
81
82 var buildTask = task(BUILD_TASK, DefaultTask.class, t -> {
83 t.dependsOn(writeEnvTask);
84 });
85
86 var stopTask = task(COMPOSE_STOP_TASK, ComposeStop.class, t -> {
87 // stop must run after build
88 t.mustRunAfter(buildTask);
89
90 t.getProfiles().addAll(composeProfiles);
91 t.getComposeFile().set(composeFile);
92 });
93
94 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
95 // rm must run after build and stop
96 t.mustRunAfter(buildTask, stopTask);
97
98 t.getProfiles().addAll(composeProfiles);
99 t.getComposeFile().set(composeFile);
100 });
101
102 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
103 t.dependsOn(buildTask);
104 // up must run after stop and rm
105 t.mustRunAfter(stopTask, rmTask);
106
107 t.getProfiles().addAll(composeProfiles);
108 t.getComposeFile().set(composeFile);
109 });
110 }
111
112 /**
113 * Processed the configurations, extracts composeVar extra property from
114 * each dependency in this configuration and adds a value to the resulting
115 * map. The values in the nap will contain image tags.
116 */
117 private Map<String, String> extractComposeEnv(Configuration config) {
118 if (config.getState() != State.UNRESOLVED) {
119 getLogger().error("extractComposeEnv: The configuration {} isn't resolved.", config.getName());
120 throw new IllegalStateException("The specified configuration isn't resolved");
121 }
122
123 getLogger().info("extractComposeEnv {}", config.getName());
124
125 var map = new HashMap<String, String>();
126
127 for (var dependency : config.getDependencies()) {
128 // get extra composeVar if present
129 extra(dependency, COMPOSE_VAR, String.class).optional().ifPresent(varName -> {
130 // if we have a composeVar extra attribute on this dependency
131
132 // get files for the dependency
133 var files = config.files(dependency);
134 if (files.size() == 1) {
135 // should bw exactly 1 file
136 var file = files.stream().findAny().get();
137 getLogger().info("Processing {}: {}", dependency, file);
138
139 try {
140 // try read imageRef
141 Utils.readImageRef(file).getTag()
142 .ifPresentOrElse(
143 tag -> map.put(varName, tag),
144 () -> getLogger().error("ImageRef doesn't have a tag: {}", file));
145
146 } catch (IOException e) {
147 getLogger().error("Failed to read ImageRef {}: {}", file, e);
148 }
149
150 } else {
151 getLogger().warn("Dependency {} must have exactly 1 file", dependency);
152 }
153 });
154 }
155
156 return map;
157 }
158
159 }
@@ -0,0 +1,24
1 package org.implab.gradle.containers;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.artifacts.Configuration;
5
6 public final class Configurations {
7
8 public static final Action<Configuration> RESOLVABLE = Configurations::resolvable;
9
10 public static final Action<Configuration> CONSUMABLE = Configurations::consumable;
11
12 private Configurations() {
13 }
14
15 private static void resolvable(Configuration configuration) {
16 configuration.setCanBeResolved(true);
17 configuration.setCanBeConsumed(false);
18 }
19
20 private static void consumable(Configuration configuration) {
21 configuration.setCanBeResolved(false);
22 configuration.setCanBeConsumed(true);
23 }
24 }
@@ -0,0 +1,69
1 package org.implab.gradle.containers;
2
3 import java.util.Collections;
4 import java.util.Optional;
5
6 import javax.inject.Inject;
7
8 import org.gradle.api.Action;
9 import org.gradle.api.NamedDomainObjectProvider;
10 import org.gradle.api.Plugin;
11 import org.gradle.api.Project;
12 import org.gradle.api.Task;
13 import org.gradle.api.artifacts.Configuration;
14 import org.gradle.api.file.Directory;
15 import org.gradle.api.tasks.TaskProvider;
16 import org.implab.gradle.containers.dsl.ExtraProps;
17 import org.implab.gradle.containers.dsl.MapEntry;
18
19 /** Project configuration traits */
20 public interface ProjectMixin {
21 @Inject
22 Project getProject();
23
24 /** registers the new task */
25 default <T extends Task> TaskProvider<T> task(String name, Class<T> clazz, Action<? super T> configure) {
26 return getProject().getTasks().register(name, clazz, configure);
27 }
28
29 /** Registers the new configuration */
30 default NamedDomainObjectProvider<Configuration> configuration(String name, Action<? super Configuration> configure) {
31 return getProject().getConfigurations().register(name, configure);
32 }
33
34 /** Returns the project directory */
35 default Directory projectDirectory() {
36 return getProject().getLayout().getProjectDirectory();
37 }
38
39 /** Applies and returns the specified plugin, plugin is applied only once. */
40 default <T extends Plugin<Project>> T plugin(Class<T> clazz) {
41 getProject().getPluginManager().apply(clazz);
42 return getProject().getPlugins().findPlugin(clazz);
43 }
44
45 /** Creates and register a new project extension.
46 *
47 * @param <T> The type of the extension
48 * @param extensionName The name of the extension in the project
49 * @param clazz The class of the extension
50 * @return the newly created extension
51 */
52 default <T> T extension(String extensionName, Class<T> clazz) {
53 T extension = getProject().getObjects().newInstance(clazz);
54 getProject().getExtensions().add(extensionName, extension);
55 return extension;
56 }
57
58 /** Return extra properties container for the specified object */
59 default Optional<ExtraProps> extra(Object target) {
60 return ExtraProps.extra(target);
61 }
62
63 /** Returns accessor for the specified extra property name */
64 default <T> MapEntry<T> extra(Object target, String prop, Class<T> clazz) {
65 return ExtraProps.extra(target)
66 .map(x -> x.prop(prop, clazz))
67 .orElseGet(() -> new MapEntry<T>(Collections.emptyMap(), prop, clazz));
68 }
69 }
@@ -0,0 +1,42
1 package org.implab.gradle.containers.dsl;
2
3 import java.util.Optional;
4
5 import org.codehaus.groovy.runtime.DefaultGroovyMethods;
6 import org.gradle.api.plugins.ExtensionAware;
7 import org.gradle.api.plugins.ExtraPropertiesExtension;
8
9 public class ExtraProps {
10 private final ExtraPropertiesExtension extraProps;
11
12 protected ExtraProps(ExtraPropertiesExtension extraProps) {
13 this.extraProps = extraProps;
14 }
15
16 public <T> Optional<T> get(String key, Class<T> clazz) {
17 if (!extraProps.has(key))
18 return Optional.empty();
19
20 return Optional.ofNullable(DefaultGroovyMethods.asType(extraProps.get(key), clazz));
21 }
22
23 public <T> void put(String key, T value) {
24 extraProps.set(key, value);
25 }
26
27 public void delete(String key) {
28 extraProps.getProperties().remove(key);
29 }
30
31 public <T> MapEntry<T> prop(String key, Class<T> clazz) {
32 return new MapEntry<>(extraProps.getProperties(), key, clazz);
33 }
34
35 public static Optional<ExtraProps> extra(Object target) {
36 return target instanceof ExtensionAware
37 ? Optional.of(new ExtraProps(
38 ((ExtensionAware) target).getExtensions().getExtraProperties()))
39 : Optional.empty();
40
41 }
42 }
@@ -0,0 +1,39
1 package org.implab.gradle.containers.dsl;
2
3 import java.util.Map;
4 import java.util.Optional;
5
6 public class MapEntry<T> {
7 private final Map<String, Object> map;
8 private final String key;
9 private final Class<T> clazz;
10
11 public MapEntry(Map<String, Object> map, String key, Class<T> clazz) {
12 this.map = map;
13 this.key = key;
14 this.clazz = clazz;
15 }
16
17 public boolean has() {
18 var value = map.get(key);
19 return value != null;
20 }
21
22 public T get() {
23 if (!has())
24 throw new IllegalStateException();
25 return clazz.cast(map.get(key));
26 }
27
28 public void put(T value) {
29 map.put(key, value);
30 }
31
32 public void remove() {
33 map.remove(key);
34 }
35
36 public Optional<T> optional() {
37 return has() ? Optional.of(get()) : Optional.empty();
38 }
39 }
@@ -0,0 +1,22
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.IOException;
4
5 import org.gradle.api.provider.Property;
6 import org.gradle.api.tasks.Internal;
7 import org.gradle.api.tasks.TaskAction;
8
9 public abstract class ComposeRm extends ComposeStop {
10
11 @Internal
12 public abstract Property<Boolean> getRemoveVolumes();
13
14 public ComposeRm() {
15 getRemoveVolumes().convention(true);
16 }
17
18 @TaskAction
19 public void run() throws InterruptedException, IOException {
20 docker().composeRm(getComposeFile().get().getAsFile(), getProfiles().get(), getRemoveVolumes().get());
21 }
22 }
@@ -0,0 +1,13
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.IOException;
4
5 import org.gradle.api.tasks.TaskAction;
6
7 public abstract class ComposeStop extends ComposeTask {
8
9 @TaskAction
10 public void run() throws InterruptedException, IOException {
11 docker().composeStop(getComposeFile().getAsFile().get(), getProfiles().get());
12 }
13 } No newline at end of file
@@ -0,0 +1,33
1 package org.implab.gradle.containers.tasks;
2
3 import org.gradle.api.file.RegularFileProperty;
4 import org.gradle.api.provider.SetProperty;
5 import org.gradle.api.tasks.Internal;
6
7 /**
8 * Base task for compose subtasks like 'uo', 'rm', 'stop', etc.
9 */
10 public abstract class ComposeTask extends DockerCliTask {
11
12 /** The list of profiles to use with the compose commands */
13 @Internal
14 public abstract SetProperty<String> getProfiles();
15
16 /**
17 * The primary compose files. This task is executed regardless whether these
18 * files was changed.
19 */
20 @Internal
21 public abstract RegularFileProperty getComposeFile();
22
23 protected ComposeTask() {
24 // the task can only be evaluated if the compose file is present
25 setOnlyIf(self -> {
26 var composeFile = getComposeFile().get().getAsFile();
27 var exists = composeFile.exists();
28 getLogger().info("file: {} {}", composeFile.toString(), exists ? "exists" : "doesn't exist");
29 return exists;
30 });
31 }
32
33 }
@@ -0,0 +1,13
1 package org.implab.gradle.containers.tasks;
2
3 import java.io.IOException;
4
5 import org.gradle.api.tasks.TaskAction;
6
7 public abstract class ComposeUp extends ComposeTask {
8
9 @TaskAction
10 public void run() throws InterruptedException, IOException {
11 docker().composeUp(getComposeFile().getAsFile().get(), getProfiles().get());
12 }
13 }
@@ -0,0 +1,59
1 package org.implab.gradle.containers.tasks;
2
3 import org.gradle.api.Action;
4 import org.gradle.api.DefaultTask;
5 import org.gradle.api.file.RegularFileProperty;
6 import org.gradle.api.provider.MapProperty;
7 import org.gradle.api.tasks.Input;
8 import org.gradle.api.tasks.OutputFile;
9 import org.gradle.api.tasks.TaskAction;
10 import org.implab.gradle.containers.PropertiesMixin;
11 import org.implab.gradle.containers.cli.Utils;
12 import org.implab.gradle.containers.dsl.MapPropertyEntry;
13
14 import java.io.FileWriter;
15 import java.io.IOException;
16 import java.util.HashMap;
17 import java.util.Map;
18
19 import groovy.lang.Closure;
20
21 public abstract class WriteEnv extends DefaultTask implements PropertiesMixin {
22
23 @Input
24 public abstract MapProperty<String, String> getEnvironment();
25
26 @OutputFile
27 public abstract RegularFileProperty getEnvFile();
28
29 public void env(Action<Map<String, String>> spec) {
30 getEnvironment().putAll(provider(() -> {
31 Map<String, String> args = new HashMap<>();
32 spec.execute(args);
33 getLogger().info("add buildArgs {}", args);
34 return args;
35 }));
36 }
37
38 public void env(Closure<Map<String, String>> spec) {
39 env(Utils.wrapClosure(spec));
40 }
41
42 public MapPropertyEntry<String, String> env(String key) {
43 return new MapPropertyEntry<String, String>(getEnvironment(), key, getProviders());
44 }
45
46 @TaskAction
47 public void run() throws IOException {
48 try (var writer = new FileWriter(getEnvFile().get().getAsFile())) {
49 for (var entry : getEnvironment().get().entrySet()) {
50 writer.write(entry.getKey());
51 writer.write("=");
52 writer.write(entry.getValue().replace("\n", "\\n"));
53 writer.write("\n");
54 }
55 }
56 }
57
58
59 }
@@ -1,2 +1,2
1 group=org.implab.gradle
1 group=org.implab.gradle
2 version=1.1.2 No newline at end of file
2 version=1.2.0 No newline at end of file
@@ -31,6 +31,7 public class ContainerBasePlugin impleme
31
31
32 containerExtension = project.getObjects().newInstance(ContainerExtension.class);
32 containerExtension = project.getObjects().newInstance(ContainerExtension.class);
33
33
34 // TODO: move properties initialization into the constructor
34 containerExtension.getImageAuthority()
35 containerExtension.getImageAuthority()
35 .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority")));
36 .convention(project.provider(() -> (String) project.getProperties().get("imagesAuthority")));
36
37
@@ -6,6 +6,7 import java.util.ArrayList;
6 import java.util.Arrays;
6 import java.util.Arrays;
7 import java.util.List;
7 import java.util.List;
8 import java.util.Optional;
8 import java.util.Optional;
9 import java.util.Set;
9
10
10 import org.gradle.api.logging.Logger;
11 import org.gradle.api.logging.Logger;
11
12
@@ -18,6 +19,10 public abstract class DockerTraits {
18 public final String INSPECT_COMMAND = "inspect";
19 public final String INSPECT_COMMAND = "inspect";
19 public final String IMAGE_COMMAND = "image";
20 public final String IMAGE_COMMAND = "image";
20 public final String TAG_COMMAND = "tag";
21 public final String TAG_COMMAND = "tag";
22 public final String COMPOSE_COMMAND = "compose";
23 public final String UP_COMMAND = "up";
24 public final String STOP_COMMAND = "stop";
25 public final String RM_COMMAND = "rm";
21
26
22 public abstract Logger getLogger();
27 public abstract Logger getLogger();
23
28
@@ -138,4 +143,44 public abstract class DockerTraits {
138 }
143 }
139 return false;
144 return false;
140 }
145 }
146
147 List<String> composeArgs(File primaryCompose, Set<String> profiles, String... extra) {
148 var args = new ArrayList<String>();
149
150 args.add(COMPOSE_COMMAND);
151 args.add("--file");
152 args.add(primaryCompose.getAbsolutePath());
153
154 if (profiles.size() > 0) {
155 for (var profile : profiles) {
156 args.add("--profile");
157 args.add(profile);
158 }
159 }
160
161 args.addAll(List.of(extra));
162
163 return args;
164 }
165
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)));
175 }
176
177 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
181 if (removeVolumes)
182 args.add("--volumes");
183
184 complete(startProcess(builder(args)));
185 }
141 }
186 }
@@ -4,8 +4,6 import java.io.File;
4 import java.util.Optional;
4 import java.util.Optional;
5
5
6 import org.gradle.api.DefaultTask;
6 import org.gradle.api.DefaultTask;
7 import org.gradle.api.file.Directory;
8 import org.gradle.api.file.DirectoryProperty;
9 import org.gradle.api.logging.Logger;
7 import org.gradle.api.logging.Logger;
10 import org.gradle.api.provider.Property;
8 import org.gradle.api.provider.Property;
11 import org.gradle.api.tasks.Input;
9 import org.gradle.api.tasks.Input;
@@ -19,9 +17,12 public abstract class DockerCliTask exte
19 @Input
17 @Input
20 public abstract Property<String> getCliCmd();
18 public abstract Property<String> getCliCmd();
21
19
20 /**
21 * Returns working directory for docker commands
22 */
22 @Input
23 @Input
23 @org.gradle.api.tasks.Optional
24 @org.gradle.api.tasks.Optional
24 public abstract DirectoryProperty getWorkingDirectory();
25 public abstract Property<File> getWorkingDirectory();
25
26
26 @Internal
27 @Internal
27 protected ContainerExtension getContainerExtension() {
28 protected ContainerExtension getContainerExtension() {
@@ -47,7 +48,6 public abstract class DockerCliTask exte
47 @Override
48 @Override
48 public Optional<File> getWorkingDir() {
49 public Optional<File> getWorkingDir() {
49 return getWorkingDirectory()
50 return getWorkingDirectory()
50 .map(Directory::getAsFile)
51 .map(Optional::of)
51 .map(Optional::of)
52 .getOrElse(Optional.empty());
52 .getOrElse(Optional.empty());
53 }
53 }
General Comments 0
You need to be logged in to leave comments. Login now