##// END OF EJS Templates
More documentation, TagImage task supports multiple tags, set COMPOSE_PROJECT_NAME to project.group by default
cin -
r14:ab28d6aa054e v1.2.1 default
parent child
Show More
@@ -0,0 +1,57
1 # Creating bundle from images
2
3 ## NAME
4
5 Save set of images to the single archive.
6
7 ## SYNOPSIS
8
9 ```gradle
10 plugins {
11 // task types and base extension
12 id "org.implab.gradle-container-base"
13 }
14
15 container {
16 cliCmd = "podman"
17 }
18
19 // create configuration
20 configurations {
21 bundleImages {
22 canBeResolved = true
23 canBeConsumed = false
24 }
25 }
26
27 // add dependencies to the project
28 dependencies {
29 bundleImages project(":images:foo"), project(":images:bar")
30 }
31
32 // create task to export bundle
33 task bundle(type: SaveImage) {
34 // add image refs from artifacts
35 imageRefs configurations.bundleImages
36
37 // add image name
38 exportImages.add "nginx:latest"
39 }
40 ```
41
42 ## DESCRIPTION
43
44 To create an archive with images the task of type `SaveImage` can be used. This
45 task has the following properties:
46
47 | Property | Description |
48 |-|-|
49 | `archiveFileName` | The file name of the bundle archive, defaults to `{archiveBaseName}-{archiveVersion}-{archiveClassifier}.{archiveExtension}`.|
50 | `archiveBaseName` | The base name of the archive, defaults to `{project.group}-{project.name}`. |
51 | `archiveVersion` | The archive version, defaults to `{project.version}`. |
52 | `exportImages` | A set of image names to include in the bundle. |
53
54 | Method | Description |
55 |-|-|
56 | `imageRefs(FileCollection)` | Adds a set of files with image refs to add to the bundle. |
57 | `imageRef(File)` | Adds an image name from the file with image reference. |
@@ -0,0 +1,148
1 # Compose project
2
3 ## NAME
4
5 `org.implab.gradle-container-compose` - docker compose project.
6
7 ## SYNOPSIS
8
9 ```gradle
10 plugins {
11 id "org.implab.gradle-container-compose"
12 }
13
14 dependencies {
15 composeImages project(":images:foo") {
16 ext {
17 // imageName from :images:foo will be written to .env as FOO_IMAGE var
18 composeVar = "FOO_IMAGE"
19 }
20 }
21 }
22
23 writeEnv {
24 // write additional variables to .env
25 env "DEBUG_JAVA" put "yes"
26
27 // set compose name, this variable is set to that value by default
28 env "COMPOSE_PROJECT_NAME" put project.group
29 }
30
31 // base container extension
32 container {
33 cliCmd = "podman"
34 }
35
36 // compose parameters
37 compose {
38 // add compose profiles
39 profiles.add("dev")
40
41 // set compose file name
42 // defaults to compose.yaml
43 composeFileName = "docker-compose.yaml"
44 }
45
46 ```
47
48 ## DESCRIPTION
49
50 This plugin creates a set of conventional tasks to prepare and start compose
51 project. This can be used to create and run sandbox.
52
53 These tasks are:
54
55 * `build` - prepares the context for the compose file, depends on `processResources`
56 and `writeEnv` tasks.
57 * `up` - invokes `compose up -d` in the built context and starts up the project
58 in the background.
59 * `stop` - invokes `compose stop` and stops the projects.
60 * `rm` - invokes `compose rm` and removes containers, by default temporary volumes
61 are removed with containers.
62 * `clean` - cleanups the build directory and removes built context.
63
64 Special configuration `composeImages` should be used to add a dependencies to the
65 images needed by this project. The dependencies must provide a single artifact
66 a json file containing `tag` property. This configuration well be resolved and
67 tag for each dependency will be stored in the environment variable specified with
68 `composeVar` property.
69
70 ```gradle
71 configuration {
72 composeImages file("nginx.json") {
73 ext {
74 composeVar = "NGINX_IMAGE"
75 }
76 }
77 composeImage project(":images:foo") {
78 ext {
79 composeVar = "FOO_IMAGE"
80 }
81 }
82 }
83 ```
84
85 The compose environment variables are written to the `.env` file inside the
86 context directory and will be used by `compose up` command.
87
88 ## Tasks
89
90 ### writeEnv
91
92 `type: WriteEnv, dependsOn: [configurations.composeImages, processResources]`
93
94 Inspects configuration `composeImages`, adds default `COMPOSE_PROJECT_NAME`
95 variable and writes `.env` file to the context directory.
96
97 This task provides a property `environment` of type `MapProperty<String, String>`
98 which can be used to customize the compose environment.
99
100 ```gradle
101 writeEnv {
102 // direct environment property access
103 environment.put("VAR_1", valueOrProvider)
104
105 // syntax sugar to modify environment property
106 env "VAR_2" put "value" // simple value
107 env "VAR_3" put provider { getSomeValue() } // provider
108
109 // map provider will be merged into the
110 env {
111 VAR_4 = "val1"
112 VAR_5 = getAnotherValue()
113 }
114 }
115 ```
116
117 ### processResources
118
119 `type: Copy`
120
121 Copies resources from the source directory `src/main` into the context directory `build/context`
122
123 ### up
124
125 `type: ComposeUp, dependsOn: [buildTask]`
126
127 Starts the compose project. The project is started in the background.
128
129 ### build
130
131 `type: DefaultTask, dependsOn: [writeEnvTask]`
132
133 This is a meta task used to group the set of tasks related to the build target.
134
135 ### stop
136
137 `type: ComposeStop`
138
139 Stops the current compose project.
140
141 ### rm
142
143 `type: ComposeRm`
144
145 Removes containers created from this compose project.
146
147 `removeValues` - boolean property, if set to true the task will remove all
148 temporary volumes left from containers.
@@ -1,159 +1,167
1 package org.implab.gradle.containers;
1 package org.implab.gradle.containers;
2
2
3 import java.io.IOException;
3 import java.io.IOException;
4 import java.util.HashMap;
4 import java.util.HashMap;
5 import java.util.Map;
5 import java.util.Map;
6 import java.util.Optional;
6
7
7 import org.gradle.api.DefaultTask;
8 import org.gradle.api.DefaultTask;
8 import org.gradle.api.Plugin;
9 import org.gradle.api.Plugin;
9 import org.gradle.api.Project;
10 import org.gradle.api.Project;
10 import org.gradle.api.artifacts.Configuration;
11 import org.gradle.api.artifacts.Configuration;
11 import org.gradle.api.artifacts.Configuration.State;
12 import org.gradle.api.artifacts.Configuration.State;
12 import org.gradle.api.logging.Logger;
13 import org.gradle.api.logging.Logger;
13 import org.gradle.api.tasks.Copy;
14 import org.gradle.api.tasks.Copy;
14 import org.gradle.api.tasks.Delete;
15 import org.gradle.api.tasks.Delete;
15 import org.implab.gradle.containers.cli.Utils;
16 import org.implab.gradle.containers.cli.Utils;
16 import org.implab.gradle.containers.tasks.ComposeRm;
17 import org.implab.gradle.containers.tasks.ComposeRm;
17 import org.implab.gradle.containers.tasks.ComposeStop;
18 import org.implab.gradle.containers.tasks.ComposeStop;
18 import org.implab.gradle.containers.tasks.ComposeUp;
19 import org.implab.gradle.containers.tasks.ComposeUp;
19 import org.implab.gradle.containers.tasks.WriteEnv;
20 import org.implab.gradle.containers.tasks.WriteEnv;
20
21
21 public abstract class ComposePlugin implements Plugin<Project>, ProjectMixin {
22 public abstract class ComposePlugin implements Plugin<Project>, ProjectMixin {
22 public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages";
23 public final String COMPOSE_IMAGES_CONFIGURATION = "composeImages";
23
24
25 public final String COMPOSE_PROJECT_NAME = "COMPOSE_PROJECT_NAME";
26
24 public final String COMPOSE_EXTENSION = "compose";
27 public final String COMPOSE_EXTENSION = "compose";
25
28
26 public final String COMPOSE_UP_TASK = "up";
29 public final String COMPOSE_UP_TASK = "up";
27
30
28 public final String COMPOSE_STOP_TASK = "stop";
31 public final String COMPOSE_STOP_TASK = "stop";
29
32
30 public final String COMPOSE_RM_TASK = "rm";
33 public final String COMPOSE_RM_TASK = "rm";
31
34
32 public final String CLEAN_TASK = "clean";
35 public final String CLEAN_TASK = "clean";
33
36
34 public final String BUILD_TASK = "build";
37 public final String BUILD_TASK = "build";
35
38
36 public final String PROCESS_RESOURCES_TASK = "processResources";
39 public final String PROCESS_RESOURCES_TASK = "processResources";
37
40
38 public final String WRITE_ENV_TASK = "writeEnv";
41 public final String WRITE_ENV_TASK = "writeEnv";
39
42
40 public final String COMPOSE_VAR = "composeVar";
43 public final String COMPOSE_VAR = "composeVar";
41
44
42 public final String ENV_FILE_NAME = ".env";
45 public final String ENV_FILE_NAME = ".env";
43
46
44 public Logger getLogger() {
47 public Logger getLogger() {
45 return getProject().getLogger();
48 return getProject().getLogger();
46 }
49 }
47
50
48 @Override
51 @Override
49 public void apply(Project project) {
52 public void apply(Project project) {
50 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
53 var containerImages = configuration(COMPOSE_IMAGES_CONFIGURATION, Configurations.RESOLVABLE);
51
54
52 // basic configuration, register extension
55 // basic configuration, register extension
53 var basePlugin = plugin(ContainerBasePlugin.class);
56 var basePlugin = plugin(ContainerBasePlugin.class);
54 var containerExtension = basePlugin.getContainerExtension();
57 var containerExtension = basePlugin.getContainerExtension();
55
58
56 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
59 var composeExtension = extension(COMPOSE_EXTENSION, ComposeExtension.class);
57
60
58 var composeFile = containerExtension.getContextDirectory()
61 var composeFile = containerExtension.getContextDirectory()
59 .file(composeExtension.getComposeFileName());
62 .file(composeExtension.getComposeFileName());
60 var composeProfiles = composeExtension.getProfiles();
63 var composeProfiles = composeExtension.getProfiles();
61
64
62 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
65 var cleanTask = task(CLEAN_TASK, Delete.class, t -> {
63 t.delete(containerExtension.getContextDirectory());
66 t.delete(containerExtension.getContextDirectory());
64 });
67 });
65
68
66 // copy task from src/main
69 // copy task from src/main
67 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
70 var processResources = task(PROCESS_RESOURCES_TASK, Copy.class, t -> {
68 t.mustRunAfter(cleanTask);
71 t.mustRunAfter(cleanTask);
69 t.from(projectDirectory().dir("src/main"));
72 t.from(projectDirectory().dir("src/main"));
70 t.into(containerExtension.getContextDirectory());
73 t.into(containerExtension.getContextDirectory());
71 });
74 });
72
75
73 // write .env
76 // write .env
74 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
77 var writeEnvTask = task(WRITE_ENV_TASK, WriteEnv.class, t -> {
75 t.dependsOn(processResources, containerImages);
78 t.dependsOn(processResources, containerImages);
76 t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME));
79 t.getEnvFile().set(containerExtension.getContextDirectory().file(ENV_FILE_NAME));
77
80
81 var group = project.getGroup();
82 if (group != null && group.toString().length() > 0) {
83 t.getEnvironment().put(COMPOSE_PROJECT_NAME, group.toString());
84 }
85
78 t.getEnvironment().putAll(containerImages.map(this::extractComposeEnv));
86 t.getEnvironment().putAll(containerImages.map(this::extractComposeEnv));
79
87
80 });
88 });
81
89
82 var buildTask = task(BUILD_TASK, DefaultTask.class, t -> {
90 var buildTask = task(BUILD_TASK, DefaultTask.class, t -> {
83 t.dependsOn(writeEnvTask);
91 t.dependsOn(writeEnvTask);
84 });
92 });
85
93
86 var stopTask = task(COMPOSE_STOP_TASK, ComposeStop.class, t -> {
94 var stopTask = task(COMPOSE_STOP_TASK, ComposeStop.class, t -> {
87 // stop must run after build
95 // stop must run after build
88 t.mustRunAfter(buildTask);
96 t.mustRunAfter(buildTask);
89
97
90 t.getProfiles().addAll(composeProfiles);
98 t.getProfiles().addAll(composeProfiles);
91 t.getComposeFile().set(composeFile);
99 t.getComposeFile().set(composeFile);
92 });
100 });
93
101
94 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
102 var rmTask = task(COMPOSE_RM_TASK, ComposeRm.class, t -> {
95 // rm must run after build and stop
103 // rm must run after build and stop
96 t.mustRunAfter(buildTask, stopTask);
104 t.mustRunAfter(buildTask, stopTask);
97
105
98 t.getProfiles().addAll(composeProfiles);
106 t.getProfiles().addAll(composeProfiles);
99 t.getComposeFile().set(composeFile);
107 t.getComposeFile().set(composeFile);
100 });
108 });
101
109
102 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
110 task(COMPOSE_UP_TASK, ComposeUp.class, t -> {
103 t.dependsOn(buildTask);
111 t.dependsOn(buildTask);
104 // up must run after stop and rm
112 // up must run after stop and rm
105 t.mustRunAfter(stopTask, rmTask);
113 t.mustRunAfter(stopTask, rmTask);
106
114
107 t.getProfiles().addAll(composeProfiles);
115 t.getProfiles().addAll(composeProfiles);
108 t.getComposeFile().set(composeFile);
116 t.getComposeFile().set(composeFile);
109 });
117 });
110 }
118 }
111
119
112 /**
120 /**
113 * Processed the configurations, extracts composeVar extra property from
121 * Processed the configurations, extracts composeVar extra property from
114 * each dependency in this configuration and adds a value to the resulting
122 * each dependency in this configuration and adds a value to the resulting
115 * map. The values in the nap will contain image tags.
123 * map. The values in the nap will contain image tags.
116 */
124 */
117 private Map<String, String> extractComposeEnv(Configuration config) {
125 private Map<String, String> extractComposeEnv(Configuration config) {
118 if (config.getState() != State.UNRESOLVED) {
126 if (config.getState() != State.UNRESOLVED) {
119 getLogger().error("extractComposeEnv: The configuration {} isn't resolved.", config.getName());
127 getLogger().error("extractComposeEnv: The configuration {} isn't resolved.", config.getName());
120 throw new IllegalStateException("The specified configuration isn't resolved");
128 throw new IllegalStateException("The specified configuration isn't resolved");
121 }
129 }
122
130
123 getLogger().info("extractComposeEnv {}", config.getName());
131 getLogger().info("extractComposeEnv {}", config.getName());
124
132
125 var map = new HashMap<String, String>();
133 var map = new HashMap<String, String>();
126
134
127 for (var dependency : config.getDependencies()) {
135 for (var dependency : config.getDependencies()) {
128 // get extra composeVar if present
136 // get extra composeVar if present
129 extra(dependency, COMPOSE_VAR, String.class).optional().ifPresent(varName -> {
137 extra(dependency, COMPOSE_VAR, String.class).optional().ifPresent(varName -> {
130 // if we have a composeVar extra attribute on this dependency
138 // if we have a composeVar extra attribute on this dependency
131
139
132 // get files for the dependency
140 // get files for the dependency
133 var files = config.files(dependency);
141 var files = config.files(dependency);
134 if (files.size() == 1) {
142 if (files.size() == 1) {
135 // should bw exactly 1 file
143 // should bw exactly 1 file
136 var file = files.stream().findAny().get();
144 var file = files.stream().findAny().get();
137 getLogger().info("Processing {}: {}", dependency, file);
145 getLogger().info("Processing {}: {}", dependency, file);
138
146
139 try {
147 try {
140 // try read imageRef
148 // try read imageRef
141 Utils.readImageRef(file).getTag()
149 Utils.readImageRef(file).getTag()
142 .ifPresentOrElse(
150 .ifPresentOrElse(
143 tag -> map.put(varName, tag),
151 tag -> map.put(varName, tag),
144 () -> getLogger().error("ImageRef doesn't have a tag: {}", file));
152 () -> getLogger().error("ImageRef doesn't have a tag: {}", file));
145
153
146 } catch (IOException e) {
154 } catch (IOException e) {
147 getLogger().error("Failed to read ImageRef {}: {}", file, e);
155 getLogger().error("Failed to read ImageRef {}: {}", file, e);
148 }
156 }
149
157
150 } else {
158 } else {
151 getLogger().warn("Dependency {} must have exactly 1 file", dependency);
159 getLogger().warn("Dependency {} must have exactly 1 file", dependency);
152 }
160 }
153 });
161 });
154 }
162 }
155
163
156 return map;
164 return map;
157 }
165 }
158
166
159 }
167 }
@@ -1,186 +1,186
1 package org.implab.gradle.containers.cli;
1 package org.implab.gradle.containers.cli;
2
2
3 import java.io.File;
3 import java.io.File;
4 import java.io.IOException;
4 import java.io.IOException;
5 import java.util.ArrayList;
5 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 import java.util.Set;
10
10
11 import org.gradle.api.logging.Logger;
11 import org.gradle.api.logging.Logger;
12
12
13 public abstract class DockerTraits {
13 public abstract class DockerTraits {
14
14
15 public final String BUILD_COMMAND = "build";
15 public final String BUILD_COMMAND = "build";
16 public final String PUSH_COMMAND = "push";
16 public final String PUSH_COMMAND = "push";
17 public final String RUN_COMMAND = "run";
17 public final String RUN_COMMAND = "run";
18 public final String SAVE_COMMAND = "save";
18 public final String SAVE_COMMAND = "save";
19 public final String INSPECT_COMMAND = "inspect";
19 public final String INSPECT_COMMAND = "inspect";
20 public final String IMAGE_COMMAND = "image";
20 public final String IMAGE_COMMAND = "image";
21 public final String TAG_COMMAND = "tag";
21 public final String TAG_COMMAND = "tag";
22 public final String COMPOSE_COMMAND = "compose";
22 public final String COMPOSE_COMMAND = "compose";
23 public final String UP_COMMAND = "up";
23 public final String UP_COMMAND = "up";
24 public final String STOP_COMMAND = "stop";
24 public final String STOP_COMMAND = "stop";
25 public final String RM_COMMAND = "rm";
25 public final String RM_COMMAND = "rm";
26
26
27 public abstract Logger getLogger();
27 public abstract Logger getLogger();
28
28
29 public abstract Optional<File> getWorkingDir();
29 public abstract Optional<File> getWorkingDir();
30
30
31 public abstract String getCliCmd();
31 public abstract String getCliCmd();
32
32
33 Process startProcess(ProcessBuilder builder) throws IOException {
33 Process startProcess(ProcessBuilder builder) throws IOException {
34 getLogger().info("Starting: {}", builder.command());
34 getLogger().info("Starting: {}", builder.command());
35 return builder.start();
35 return builder.start();
36 }
36 }
37
37
38 protected boolean checkRetCode(Process proc, int code) throws InterruptedException {
38 protected boolean checkRetCode(Process proc, int code) throws InterruptedException {
39 if (getLogger().isInfoEnabled()) {
39 if (getLogger().isInfoEnabled()) {
40 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
40 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
41 Utils.redirectIO(proc.getErrorStream(), getLogger()::info);
41 Utils.redirectIO(proc.getErrorStream(), getLogger()::info);
42 }
42 }
43
43
44 return proc.waitFor() == code;
44 return proc.waitFor() == code;
45 }
45 }
46
46
47 protected void complete(Process proc) throws InterruptedException, IOException {
47 protected void complete(Process proc) throws InterruptedException, IOException {
48 if (getLogger().isInfoEnabled())
48 if (getLogger().isInfoEnabled())
49 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
49 Utils.redirectIO(proc.getInputStream(), getLogger()::info);
50
50
51 if (getLogger().isErrorEnabled())
51 if (getLogger().isErrorEnabled())
52 Utils.redirectIO(proc.getErrorStream(), getLogger()::error);
52 Utils.redirectIO(proc.getErrorStream(), getLogger()::error);
53
53
54 var code = proc.waitFor();
54 var code = proc.waitFor();
55 if (code != 0) {
55 if (code != 0) {
56 getLogger().error("The process exited with code {}", code);
56 getLogger().error("The process exited with code {}", code);
57 throw new IOException("The process exited with error code " + code);
57 throw new IOException("The process exited with error code " + code);
58 }
58 }
59 }
59 }
60
60
61 protected ProcessBuilder builder(String... args) {
61 protected ProcessBuilder builder(String... args) {
62 var argsList = new ArrayList<String>(args.length + 1);
62 var argsList = new ArrayList<String>(args.length + 1);
63 Arrays.stream(args).forEach(argsList::add);
63 Arrays.stream(args).forEach(argsList::add);
64
64
65 return builder(argsList);
65 return builder(argsList);
66 }
66 }
67
67
68 protected ProcessBuilder builder(List<String> args) {
68 protected ProcessBuilder builder(List<String> args) {
69 var command = new ArrayList<String>(args.size() + 1);
69 var command = new ArrayList<String>(args.size() + 1);
70
70
71 command.add(getCliCmd());
71 command.add(getCliCmd());
72 args.forEach(command::add);
72 args.forEach(command::add);
73
73
74 var builder = new ProcessBuilder(command);
74 var builder = new ProcessBuilder(command);
75
75
76 getWorkingDir().ifPresent(builder::directory);
76 getWorkingDir().ifPresent(builder::directory);
77 return builder;
77 return builder;
78 }
78 }
79
79
80 public void buildImage(String imageName, File contextDirectory, List<String> options)
80 public void buildImage(String imageName, File contextDirectory, List<String> options)
81 throws IOException, InterruptedException {
81 throws IOException, InterruptedException {
82 var args = new ArrayList<String>();
82 var args = new ArrayList<String>();
83 args.add(BUILD_COMMAND);
83 args.add(BUILD_COMMAND);
84 args.addAll(options);
84 args.addAll(options);
85 args.add("-t");
85 args.add("-t");
86 args.add(imageName);
86 args.add(imageName);
87 args.add(contextDirectory.getAbsolutePath());
87 args.add(contextDirectory.getAbsolutePath());
88 complete(startProcess(builder(args)));
88 complete(startProcess(builder(args)));
89 }
89 }
90
90
91 public void pushImage(String image, List<String> options) throws InterruptedException, IOException {
91 public void pushImage(String image, List<String> options) throws InterruptedException, IOException {
92 var args = new ArrayList<String>();
92 var args = new ArrayList<String>();
93 args.add(PUSH_COMMAND);
93 args.add(PUSH_COMMAND);
94 args.addAll(options);
94 args.addAll(options);
95 args.add(image);
95 args.add(image);
96 complete(startProcess(builder(args)));
96 complete(startProcess(builder(args)));
97 }
97 }
98
98
99 public void runImage(String image, List<String> options, List<String> command)
99 public void runImage(String image, List<String> options, List<String> command)
100 throws InterruptedException, IOException {
100 throws InterruptedException, IOException {
101 var args = new ArrayList<String>();
101 var args = new ArrayList<String>();
102 args.add(RUN_COMMAND);
102 args.add(RUN_COMMAND);
103 args.addAll(options);
103 args.addAll(options);
104 args.add(image);
104 args.add(image);
105 args.addAll(command);
105 args.addAll(command);
106 complete(startProcess(builder(args)));
106 complete(startProcess(builder(args)));
107 }
107 }
108
108
109 public void saveImage(List<String> images, File output) throws InterruptedException, IOException {
109 public void saveImage(Set<String> images, File output) throws InterruptedException, IOException {
110 if (output.exists())
110 if (output.exists())
111 output.delete();
111 output.delete();
112
112
113 var args = new ArrayList<String>();
113 var args = new ArrayList<String>();
114 args.add(SAVE_COMMAND);
114 args.add(SAVE_COMMAND);
115 args.add("-o");
115 args.add("-o");
116 args.add(output.getAbsolutePath());
116 args.add(output.getAbsolutePath());
117 images.forEach(args::add);
117 images.forEach(args::add);
118
118
119 complete(startProcess(builder(args)));
119 complete(startProcess(builder(args)));
120 }
120 }
121
121
122 public void tagImage(String source, String target) throws InterruptedException, IOException {
122 public void tagImage(String source, String target) throws InterruptedException, IOException {
123 complete(startProcess(builder(TAG_COMMAND, source, target)));
123 complete(startProcess(builder(TAG_COMMAND, source, target)));
124 }
124 }
125
125
126 public boolean imageExists(String imageId) throws InterruptedException, IOException {
126 public boolean imageExists(String imageId) throws InterruptedException, IOException {
127 getLogger().info("Check image {} exists", imageId);
127 getLogger().info("Check image {} exists", imageId);
128
128
129 return checkRetCode(
129 return checkRetCode(
130 startProcess(builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId)),
130 startProcess(builder(IMAGE_COMMAND, INSPECT_COMMAND, "--format", "image-exists", imageId)),
131 0);
131 0);
132 }
132 }
133
133
134 public boolean imageExists(File imageIdFile) {
134 public boolean imageExists(File imageIdFile) {
135 if (imageIdFile.exists()) {
135 if (imageIdFile.exists()) {
136 try {
136 try {
137 var imageId = Utils.readImageRef(imageIdFile);
137 var imageId = Utils.readImageRef(imageIdFile);
138 return imageExists(imageId.getId());
138 return imageExists(imageId.getId());
139 } catch (IOException | InterruptedException e) {
139 } catch (IOException | InterruptedException e) {
140 getLogger().error("Failed to read imageId {}: {}", imageIdFile, e);
140 getLogger().error("Failed to read imageId {}: {}", imageIdFile, e);
141 return false;
141 return false;
142 }
142 }
143 }
143 }
144 return false;
144 return false;
145 }
145 }
146
146
147 List<String> composeArgs(File primaryCompose, Set<String> profiles, String... extra) {
147 List<String> composeArgs(File primaryCompose, Set<String> profiles, String... extra) {
148 var args = new ArrayList<String>();
148 var args = new ArrayList<String>();
149
149
150 args.add(COMPOSE_COMMAND);
150 args.add(COMPOSE_COMMAND);
151 args.add("--file");
151 args.add("--file");
152 args.add(primaryCompose.getAbsolutePath());
152 args.add(primaryCompose.getAbsolutePath());
153
153
154 if (profiles.size() > 0) {
154 if (profiles.size() > 0) {
155 for (var profile : profiles) {
155 for (var profile : profiles) {
156 args.add("--profile");
156 args.add("--profile");
157 args.add(profile);
157 args.add(profile);
158 }
158 }
159 }
159 }
160
160
161 args.addAll(List.of(extra));
161 args.addAll(List.of(extra));
162
162
163 return args;
163 return args;
164 }
164 }
165
165
166 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
166 public void composeUp(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
167 var args = composeArgs(primaryCompose, profiles, UP_COMMAND, "--detach");
167 var args = composeArgs(primaryCompose, profiles, UP_COMMAND, "--detach");
168
168
169 complete(startProcess(builder(args)));
169 complete(startProcess(builder(args)));
170 }
170 }
171
171
172 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
172 public void composeStop(File primaryCompose, Set<String> profiles) throws InterruptedException, IOException {
173 var args = composeArgs(primaryCompose, profiles, STOP_COMMAND);
173 var args = composeArgs(primaryCompose, profiles, STOP_COMMAND);
174 complete(startProcess(builder(args)));
174 complete(startProcess(builder(args)));
175 }
175 }
176
176
177 public void composeRm(File primaryCompose, Set<String> profiles, boolean removeVolumes)
177 public void composeRm(File primaryCompose, Set<String> profiles, boolean removeVolumes)
178 throws InterruptedException, IOException {
178 throws InterruptedException, IOException {
179 var args = composeArgs(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
179 var args = composeArgs(primaryCompose, profiles, RM_COMMAND, "--force", "--stop");
180
180
181 if (removeVolumes)
181 if (removeVolumes)
182 args.add("--volumes");
182 args.add("--volumes");
183
183
184 complete(startProcess(builder(args)));
184 complete(startProcess(builder(args)));
185 }
185 }
186 }
186 }
@@ -1,111 +1,111
1 package org.implab.gradle.containers.tasks;
1 package org.implab.gradle.containers.tasks;
2
2
3 import java.io.File;
3 import java.io.File;
4 import java.io.IOException;
4 import java.io.IOException;
5 import java.util.ArrayList;
5 import java.util.ArrayList;
6 import java.util.Optional;
6 import java.util.Optional;
7
7
8 import org.gradle.api.file.DirectoryProperty;
8 import org.gradle.api.file.DirectoryProperty;
9 import org.gradle.api.file.FileCollection;
9 import org.gradle.api.file.FileCollection;
10 import org.gradle.api.file.RegularFile;
10 import org.gradle.api.file.RegularFile;
11 import org.gradle.api.provider.ListProperty;
12 import org.gradle.api.provider.Property;
11 import org.gradle.api.provider.Property;
13 import org.gradle.api.provider.Provider;
12 import org.gradle.api.provider.Provider;
13 import org.gradle.api.provider.SetProperty;
14 import org.gradle.api.tasks.Input;
14 import org.gradle.api.tasks.Input;
15 import org.gradle.api.tasks.Internal;
15 import org.gradle.api.tasks.Internal;
16 import org.gradle.api.tasks.OutputFile;
16 import org.gradle.api.tasks.OutputFile;
17 import org.gradle.api.tasks.TaskAction;
17 import org.gradle.api.tasks.TaskAction;
18 import org.gradle.api.tasks.TaskExecutionException;
18 import org.gradle.api.tasks.TaskExecutionException;
19 import org.implab.gradle.containers.cli.Utils;
19 import org.implab.gradle.containers.cli.Utils;
20
20
21 public abstract class SaveImage extends DockerCliTask {
21 public abstract class SaveImage extends DockerCliTask {
22
22
23 @Input
23 @Input
24 public abstract ListProperty<String> getExportImages();
24 public abstract SetProperty<String> getExportImages();
25
25
26 @OutputFile
26 @OutputFile
27 public Provider<RegularFile> getArchiveFile() {
27 public Provider<RegularFile> getArchiveFile() {
28 return getDestinationDirectory().file(getArchiveFileName());
28 return getDestinationDirectory().file(getArchiveFileName());
29 }
29 }
30
30
31 @Internal
31 @Internal
32 public abstract DirectoryProperty getDestinationDirectory();
32 public abstract DirectoryProperty getDestinationDirectory();
33
33
34 @Internal
34 @Internal
35 public abstract Property<String> getArchiveFileName();
35 public abstract Property<String> getArchiveFileName();
36
36
37 @Internal
37 @Internal
38 public abstract Property<String> getArchiveBaseName();
38 public abstract Property<String> getArchiveBaseName();
39
39
40 @Internal
40 @Internal
41 public abstract Property<String> getArchiveVersion();
41 public abstract Property<String> getArchiveVersion();
42
42
43 @Internal
43 @Internal
44 public abstract Property<String> getArchiveClassifier();
44 public abstract Property<String> getArchiveClassifier();
45
45
46 @Internal
46 @Internal
47 public abstract Property<String> getArchiveExtension();
47 public abstract Property<String> getArchiveExtension();
48
48
49 String readImageRefTag(File file) throws Exception {
49 String readImageRefTag(File file) throws Exception {
50 getLogger().info("Reading image ref from {}", file);
50 getLogger().info("Reading image ref from {}", file);
51 var imageRef = Utils.readImageRef(file);
51 var imageRef = Utils.readImageRef(file);
52 return imageRef.getTag().orElseThrow(() -> new Exception("The image tag is required to save image"));
52 return imageRef.getTag().orElseThrow(() -> new Exception("The image tag is required to save image"));
53 }
53 }
54
54
55 public void imageRef(File file) {
55 public void imageRef(File file) {
56 getExportImages().add(provider(() -> {
56 getExportImages().add(provider(() -> {
57 return readImageRefTag(file);
57 return readImageRefTag(file);
58 }));
58 }));
59 }
59 }
60
60
61 public void imageRefs(FileCollection files) {
61 public void imageRefs(FileCollection files) {
62 dependsOn(files);
62 dependsOn(files);
63 getExportImages().addAll(provider(() -> {
63 getExportImages().addAll(provider(() -> {
64 var tags = new ArrayList<String>();
64 var tags = new ArrayList<String>();
65 for (File file : files) {
65 for (File file : files) {
66 tags.add(readImageRefTag(file));
66 tags.add(readImageRefTag(file));
67 }
67 }
68 return tags;
68 return tags;
69 }));
69 }));
70 }
70 }
71
71
72 public SaveImage() {
72 public SaveImage() {
73 getArchiveFileName().convention(provider(this::conventionalArchiveFileName));
73 getArchiveFileName().convention(provider(this::conventionalArchiveFileName));
74 getArchiveBaseName().convention(provider(this::conventionalArchiveBaseName));
74 getArchiveBaseName().convention(provider(this::conventionalArchiveBaseName));
75 getArchiveVersion().convention(provider(() -> getProject().getVersion()).map(x -> x.toString()));
75 getArchiveVersion().convention(provider(() -> getProject().getVersion()).map(x -> x.toString()));
76 getDestinationDirectory().convention(getProject().getLayout().getBuildDirectory());
76 getDestinationDirectory().convention(getProject().getLayout().getBuildDirectory());
77 getArchiveExtension().convention("tar");
77 getArchiveExtension().convention("tar");
78 }
78 }
79
79
80 private String conventionalArchiveBaseName() {
80 private String conventionalArchiveBaseName() {
81 ArrayList<String> parts = new ArrayList<>();
81 ArrayList<String> parts = new ArrayList<>();
82 Optional.of(getProject().getGroup()).map(x -> x.toString()).ifPresent(parts::add);
82 Optional.of(getProject().getGroup()).map(x -> x.toString()).ifPresent(parts::add);
83 parts.add(getProject().getName());
83 parts.add(getProject().getName());
84 return String.join("-", parts);
84 return String.join("-", parts);
85 }
85 }
86
86
87 private String conventionalArchiveFileName() {
87 private String conventionalArchiveFileName() {
88 ArrayList<String> parts = new ArrayList<>();
88 ArrayList<String> parts = new ArrayList<>();
89
89
90 if (getArchiveBaseName().isPresent())
90 if (getArchiveBaseName().isPresent())
91 parts.add(getArchiveBaseName().get());
91 parts.add(getArchiveBaseName().get());
92
92
93 if (getArchiveVersion().isPresent())
93 if (getArchiveVersion().isPresent())
94 parts.add(getArchiveVersion().get());
94 parts.add(getArchiveVersion().get());
95
95
96 if (getArchiveClassifier().isPresent())
96 if (getArchiveClassifier().isPresent())
97 parts.add(getArchiveClassifier().get());
97 parts.add(getArchiveClassifier().get());
98
98
99 if (parts.size() == 0)
99 if (parts.size() == 0)
100 throw new TaskExecutionException(this, new Exception("The archiveFileName ism't specified"));
100 throw new TaskExecutionException(this, new Exception("The archiveFileName ism't specified"));
101
101
102 return String.join("-", parts) + "." + getArchiveExtension().get();
102 return String.join("-", parts) + "." + getArchiveExtension().get();
103 }
103 }
104
104
105 @TaskAction
105 @TaskAction
106 public void run() throws InterruptedException, IOException {
106 public void run() throws InterruptedException, IOException {
107 docker().saveImage(
107 docker().saveImage(
108 getExportImages().get(),
108 getExportImages().get(),
109 getArchiveFile().map(RegularFile::getAsFile).get());
109 getArchiveFile().map(RegularFile::getAsFile).get());
110 }
110 }
111 }
111 }
@@ -1,21 +1,46
1 package org.implab.gradle.containers.tasks;
1 package org.implab.gradle.containers.tasks;
2
2
3 import java.io.IOException;
3 import java.io.IOException;
4 import java.util.HashSet;
5 import java.util.Set;
6
4 import org.gradle.api.provider.Property;
7 import org.gradle.api.provider.Property;
8 import org.gradle.api.provider.SetProperty;
5 import org.gradle.api.tasks.Input;
9 import org.gradle.api.tasks.Input;
6 import org.gradle.api.tasks.TaskAction;
10 import org.gradle.api.tasks.TaskAction;
7
11
8 public abstract class TagImage extends DockerCliTask {
12 public abstract class TagImage extends DockerCliTask {
9 @Input
13 @Input
10 public abstract Property<String> getSrcImage();
14 public abstract Property<String> getSrcImage();
11
15
12 @Input
16 @Input
17 public abstract SetProperty<String> getTags();
18
19 @Input
20 @Deprecated
13 public abstract Property<String> getDestImage();
21 public abstract Property<String> getDestImage();
14
22
23 private Set<String> getImageTags() {
24 var tags = new HashSet<>(getTags().get());
25 tags.add(getDestImage().get());
26 return tags;
27 }
28
29 public TagImage() {
30 this.setOnlyIf("No tags were specified", self -> getImageTags().size() > 0);
31 }
32
15 @TaskAction
33 @TaskAction
16 public void run() throws InterruptedException, IOException {
34 public void run() throws InterruptedException, IOException {
17 docker().tagImage(
35 var tags = getImageTags();
18 getSrcImage().get(),
36 var src = getSrcImage().get();
19 getDestImage().get());
37
38 if (tags.size() == 0)
39 getLogger().info("No tags were specified");
40
41 for (var tag : tags) {
42 getLogger().info("Tag: {}", tag);
43 docker().tagImage(src, tag);
44 }
20 }
45 }
21 }
46 }
@@ -1,110 +1,220
1 # Build and publish images with docker/podman
1 # Build and publish images with docker/podman
2
2
3 ## SYNOPSIS
3 ## SYNOPSIS
4
4
5 ```gradle
5 ```gradle
6
6
7 plugins {
7 plugins {
8 id 'org.implab.gradle-container' version '1.1'
8 id 'org.implab.gradle-container'
9 }
9 }
10
10
11 container {
11 container {
12 // if you want to use podman
12 // if you want to use podman
13 cliCmd = "podman"
13 cliCmd = "podman"
14 }
14 }
15
15
16 configurations {
16 configurations {
17 app
17 app
18 }
18 }
19
19
20 dependencies {
20 dependencies {
21 // the application that needs to be built and packed
21 // the application that needs to be built and packed
22 app project(":server")
22 app project(":server")
23 }
23 }
24
24
25 // add custom task to copy application files
25 // add custom task to copy application files
26 // to the docker build context.
26 // to the docker build context.
27 task copyApp(type: Copy) {
27 task copyApp(type: Copy) {
28 processResources.dependsOn it
28 processResources.dependsOn it
29
29
30 into container.contextDirectory.dir("root/opt/myapp")
30 into container.contextDirectory.dir("root/opt/myapp")
31 from configurations.app
31 from configurations.app
32 }
32 }
33
33
34 task printVersion {
34 task printVersion {
35 doLast {
35 doLast {
36 println "tag: ${buildImage.imageTag.get()}"
36 println "tag: ${buildImage.imageTag.get()}"
37 println "archive: ${saveImage.archiveFileName.get()}"
37 println "archive: ${saveImage.archiveFileName.get()}"
38 }
38 }
39 }
39 }
40
40
41
41
42 ```
42 ```
43
43
44 ## Description
44 ## Description
45
45
46 This plugin is a simple wrapper around docker CLI. All the image
46 This plugin is a simple wrapper around docker CLI. All the image
47 building process is deligated to the `Dockerfile` which will run
47 building process is delegated to the `Dockerfile` which will run
48 in the prepeared build context.
48 in the prepared build context.
49
49
50 ### Project structure
50 ## Project structure
51
51
52 * `build/`
52 * `build/` - this folder will be created during build, it can be useful while
53 * `context/` - the build context where `docker build` command will run.
53 solving Dockerfile problems
54 * `imageid` - the file storing the id of the image has been built.
54 * `context/` - the build context where `docker build` command will run.
55 * `image-name-1.2.3.tgz` - the exported image if `saveImage` has been executed.
55 * `imageid` - the file storing the id of the image has been built.
56 * `image-name-1.2.3.tgz` - the exported image if `saveImage` has been executed.
56 * `src`
57 * `src`
57 * `main` - the source files which will be copied to the build context.
58 * `main` - the source files which will be copied to the build context.
59
60 ## Global properties
58
61
59 ## Properties
62 There are several global properties recognized by this plugin in the project.
63 These properties affect images naming and publication and they are useful in
64 multi-project environment.
60
65
61 `imagesAuthority` - the registry where the image should be published.
66 `imagesAuthority` - the registry where the image should be published.
62 for example `docker.io`
67 for example `docker.io`
63
68
64 `imagesGroup` - the path to the image in the repository.
69 `imagesGroup` - the path to the image in the repository.
65
70
71 `containerCli` - the command line cli, this property corresponds to
72 `container.cliCmd` in the project.
73
74 Properties defined in the project takes precedence over global properties.
75
76 ## Image names
77
78 ```gradle
79 plugins {
80 id "org.implab.gradle-container"
81 }
82
83 container {
84 // image authority, the repository for your images
85 // defaults to global imagesAuthority property or none
86 imageAuthority = "my.registry.org"
87
88 // the image path
89 // defaults to global imagesGroup property or none
90 imageGroup = "my/project"
91
92 // the name of the image
93 // defaults to project.name
94 imageLocalName = "foo"
95 }
96
97 // provider for imageName, returns ImageName object
98 // ImageName consists of "{imageAuthority}/{imageGroup}/{imageLocalName}"
99 def imageNameProvider = container.imageName
100 ```
101
66 ## Tasks
102 ## Tasks
67
103
104 Some tasks support passing additional options as additional command line
105 parameters. These task has the property `options` and some additional methods.
106
107 | Property | Description |
108 |--|--|
109 | `options` | A list of additional arguments passed to `docker build` command. |
110
111 | Method | Description |
112 |---|---|
113 | `option(String)` | Adds option to `options`. |
114 | `option(Closure)` | Converts the parameter to provider and adds it to `options`. |
115 | `option(Callable)` | Converts the parameter to provider and adds it to `options`. |
116 | `options(String...)` | Adds specified options to `options`. |
117 | `options(Closure)`| Converts the parameter to provider and adds it to `options`. |
118 | `options(Callable)`| Converts the parameter to provider and adds it to `options` |
119
68 ### buildImage
120 ### buildImage
69
121
122 `type: BuildImage`
123
70 The task builds the image. Wrapper around the `docker build` command.
124 The task builds the image. Wrapper around the `docker build` command.
71
125
126 | Property | Description |
127 |---|---|
128 | `contextDirectory` | A Dockerfile context directory. Set to `container.context`. |
129 | `buildArgs` | A dictionary with environment variables which are set during build. |
130 | `buildTarget` | A target image for the multi-stage builds. Defaults to none. |
131 | `imageName` | A name (tag) for the resulting image. |
132 | `imageIdFIle` | Output file name where image ref will be written. |
133
134 This task also supports additional command line options.
135
72 ### saveImage
136 ### saveImage
73
137
138 `type: SaveImage`
139
74 The task exports image as the .tar archive.
140 The task exports image as the .tar archive.
75
141
142 | Property | Description |
143 |-|-|
144 | `archiveFileName` | The file name of the bundle archive, defaults to `{archiveBaseName}-{archiveVersion}-{archiveClassifier}.{archiveExtension}`.|
145 | `archiveBaseName` | The base name of the archive, defaults to `{project.group}-{project.name}`. |
146 | `archiveVersion` | The archive version, defaults to `{project.version}`. |
147 | `exportImages` | A set of image names to include in the bundle. |
148
149 | Method | Description |
150 |-|-|
151 | `imageRefs(FileCollection)` | Adds a set of files with image refs to add to the bundle. |
152 | `imageRef(File)` | Adds an image name from the file with image reference. |
153
76 ### pushImage
154 ### pushImage
77
155
78 The task pushes the image to the remote repository.
156 The task pushes the image to the remote repository (imageAuthority).
157
158 [Since v1.2] This task also supports additional command line options. You can use them to
159 push all tags for the image.
160
161 ```gradle
162 pushImage {
163 option "--all-tags"
164 }
165 ```
79
166
80 ### processResources
167 ### processResources
81
168
82 The copy task, it prepares the build context. Use it to customize
169 The copy task, it prepares the build context. Use this task to customize
83 the build context.
170 the build context.
84
171
85 ### tagImage
172 ### tagImage
86
173
174 `type: TagImage`
175
87 since: 1.1
176 since: 1.1
88
177
89 ```gradle
178 ```gradle
90 task tagLatest(type: TagImage) {
179 task tagLatest(type: TagImage) {
180 pushImage.dependsOn it
181
91 srcImage = container.imageName
182 srcImage = container.imageName
92 destImage = container.imageName.map { it.tag("latest") }
183
184 tags.add(container.imageName.map { it.tag("latest") })
93 }
185 }
94
186
95 ```
187 ```
96
188
189 | Property | Description |
190 |-|-|
191 | `srcImage` | The source image name to add tag to. |
192 | `tags` | The set of tags to add to the image. |
193
194 ## See also
195
196 * Creating [compose](compose.md) project
197 * Creating [bundle](bundle.md) project
198
97 ## Changes
199 ## Changes
98
200
201 ### 1.2
202
203 Added `org.implab.gradle-container-base`, `org.implab.gradle-container-compose`
204 plugins.
205
206 * `org.implab.gradle-container-base` registers base extension and task types.
207 * `org.implab.gradle-container-compose` registers conventional tasks.
208
99 ### 1.1
209 ### 1.1
100
210
101 Warning! This version isn't fully backward compatible with 1.0 version.
211 Warning! This version isn't fully backward compatible with 1.0 version.
102
212
103 * Added `TagImage` task type
213 * Added `TagImage` task type
104 * `ImageTag` class is replaced with `ImageName` class
214 * `ImageTag` class is replaced with `ImageName` class
105 * `BuildImage`, `PushImage` tasks are now accepting only `imageName` property
215 * `BuildImage`, `PushImage` tasks are now accepting only `imageName` property
106 * Added `imageName`, `imageShortName`, `imageTag` properties to `container` extension
216 * Added `imageName`, `imageShortName`, `imageTag` properties to `container` extension
107
217
108 ### 1.0
218 ### 1.0
109
219
110 Initial release. Default tasks to build and publish container images.
220 Initial release. Default tasks to build and publish container images.
General Comments 0
You need to be logged in to leave comments. Login now