diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
index 0ca748e3eb04..968829caeb8b 100644
--- a/buildSrc/build.gradle.kts
+++ b/buildSrc/build.gradle.kts
@@ -44,20 +44,19 @@ dependencies {
implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.1")
implementation("com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.14")
- runtimeOnly("com.google.protobuf:protobuf-gradle-plugin:0.8.13") // Enable proto code generation
- runtimeOnly("com.github.davidmc24.gradle-avro-plugin:gradle-avro-plugin:0.16.0") // Enable Avro code generation
- runtimeOnly("com.diffplug.spotless:spotless-plugin-gradle:5.6.1") // Enable a code formatting plugin
- runtimeOnly("com.palantir.gradle.docker:gradle-docker:0.34.0") // Enable building Docker containers
- runtimeOnly("gradle.plugin.com.dorongold.plugins:task-tree:1.5") // Adds a 'taskTree' task to print task dependency tree
- runtimeOnly("gradle.plugin.com.github.johnrengelman:shadow:7.1.1") // Enable shading Java dependencies
+ runtimeOnly("com.google.protobuf:protobuf-gradle-plugin:0.8.13") // Enable proto code generation
+ runtimeOnly("com.github.davidmc24.gradle-avro-plugin:gradle-avro-plugin:0.16.0") // Enable Avro code generation
+ runtimeOnly("com.diffplug.spotless:spotless-plugin-gradle:5.6.1") // Enable a code formatting plugin
+ runtimeOnly("gradle.plugin.com.dorongold.plugins:task-tree:1.5") // Adds a 'taskTree' task to print task dependency tree
+ runtimeOnly("gradle.plugin.com.github.johnrengelman:shadow:7.1.1") // Enable shading Java dependencies
runtimeOnly("net.linguica.gradle:maven-settings-plugin:0.5")
runtimeOnly("gradle.plugin.io.pry.gradle.offline_dependencies:gradle-offline-dependencies-plugin:0.5.0") // Enable creating an offline repository
- runtimeOnly("net.ltgt.gradle:gradle-errorprone-plugin:1.2.1") // Enable errorprone Java static analysis
+ runtimeOnly("net.ltgt.gradle:gradle-errorprone-plugin:3.1.0") // Enable errorprone Java static analysis
runtimeOnly("org.ajoberstar.grgit:grgit-gradle:4.1.1") // Enable website git publish to asf-site branch
- runtimeOnly("com.avast.gradle:gradle-docker-compose-plugin:0.16.12") // Enable docker compose tasks
+ runtimeOnly("com.avast.gradle:gradle-docker-compose-plugin:0.16.12") // Enable docker compose tasks
runtimeOnly("ca.cutterslade.gradle:gradle-dependency-analyze:1.8.3") // Enable dep analysis
runtimeOnly("gradle.plugin.net.ossindex:ossindex-gradle-plugin:0.4.11") // Enable dep vulnerability analysis
- runtimeOnly("org.checkerframework:checkerframework-gradle-plugin:0.6.33") // Enable enhanced static checking plugin
+ runtimeOnly("org.checkerframework:checkerframework-gradle-plugin:0.6.33") // Enable enhanced static checking plugin
}
// Because buildSrc is built and tested automatically _before_ gradle
diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy
new file mode 100644
index 000000000000..442b35439cae
--- /dev/null
+++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerPlugin.groovy
@@ -0,0 +1,325 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.gradle
+
+import java.util.regex.Pattern
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.Task
+import org.gradle.api.file.CopySpec
+import org.gradle.api.logging.LogLevel
+import org.gradle.api.logging.Logger
+import org.gradle.api.logging.Logging
+import org.gradle.api.tasks.Copy
+import org.gradle.api.tasks.Delete
+import org.gradle.api.tasks.Exec
+
+/**
+ * A gradle plug-in interacting with docker. Originally replicated from
+ * com.palantir.docker plugin.
+ */
+class BeamDockerPlugin implements Plugin {
+ private static final Logger logger = Logging.getLogger(BeamDockerPlugin.class)
+ private static final Pattern LABEL_KEY_PATTERN = Pattern.compile('^[a-z0-9.-]*$')
+
+ static class DockerExtension {
+ Project project
+
+ private static final String DEFAULT_DOCKERFILE_PATH = 'Dockerfile'
+ String name = null
+ File dockerfile = null
+ String dockerComposeTemplate = 'docker-compose.yml.template'
+ String dockerComposeFile = 'docker-compose.yml'
+ Set dependencies = [] as Set
+ Set tags = [] as Set
+ Map namedTags = [:]
+ Map labels = [:]
+ Map buildArgs = [:]
+ boolean pull = false
+ boolean noCache = false
+ String network = null
+ boolean buildx = false
+ Set platform = [] as Set
+ boolean load = false
+ boolean push = false
+ String builder = null
+
+ File resolvedDockerfile = null
+ File resolvedDockerComposeTemplate = null
+ File resolvedDockerComposeFile = null
+
+ // The CopySpec defining the Docker Build Context files
+ final CopySpec copySpec
+
+ DockerExtension(Project project) {
+ this.project = project
+ this.copySpec = project.copySpec()
+ }
+
+ void resolvePathsAndValidate() {
+ if (dockerfile != null) {
+ resolvedDockerfile = dockerfile
+ } else {
+ resolvedDockerfile = project.file(DEFAULT_DOCKERFILE_PATH)
+ }
+ resolvedDockerComposeFile = project.file(dockerComposeFile)
+ resolvedDockerComposeTemplate = project.file(dockerComposeTemplate)
+ }
+
+ void dependsOn(Task... args) {
+ this.dependencies = args as Set
+ }
+
+ Set getDependencies() {
+ return dependencies
+ }
+
+ void files(Object... files) {
+ copySpec.from(files)
+ }
+
+ void tags(String... args) {
+ this.tags = args as Set
+ }
+
+ Set getTags() {
+ return this.tags + project.getVersion().toString()
+ }
+
+ Set getPlatform() {
+ return platform
+ }
+
+ void platform(String... args) {
+ this.platform = args as Set
+ }
+ }
+
+ @Override
+ void apply(Project project) {
+ DockerExtension ext = project.extensions.create('docker', DockerExtension, project)
+
+ Delete clean = project.tasks.create('dockerClean', Delete, {
+ group = 'Docker'
+ description = 'Cleans Docker build directory.'
+ })
+
+ Copy prepare = project.tasks.create('dockerPrepare', Copy, {
+ group = 'Docker'
+ description = 'Prepares Docker build directory.'
+ dependsOn clean
+ })
+
+ Exec exec = project.tasks.create('docker', Exec, {
+ group = 'Docker'
+ description = 'Builds Docker image.'
+ dependsOn prepare
+ })
+
+ Task tag = project.tasks.create('dockerTag', {
+ group = 'Docker'
+ description = 'Applies all tags to the Docker image.'
+ dependsOn exec
+ })
+
+ Task pushAllTags = project.tasks.create('dockerTagsPush', {
+ group = 'Docker'
+ description = 'Pushes all tagged Docker images to configured Docker Hub.'
+ })
+
+ project.tasks.create('dockerPush', {
+ group = 'Docker'
+ description = 'Pushes named Docker image to configured Docker Hub.'
+ dependsOn pushAllTags
+ })
+
+ project.afterEvaluate {
+ ext.resolvePathsAndValidate()
+ String dockerDir = "${project.buildDir}/docker"
+ clean.delete dockerDir
+
+ prepare.with {
+ with ext.copySpec
+ from(ext.resolvedDockerfile) {
+ rename { fileName ->
+ fileName.replace(ext.resolvedDockerfile.getName(), 'Dockerfile')
+ }
+ }
+ into dockerDir
+ }
+
+ exec.with {
+ workingDir dockerDir
+ commandLine buildCommandLine(ext)
+ dependsOn ext.getDependencies()
+ logging.captureStandardOutput LogLevel.INFO
+ logging.captureStandardError LogLevel.ERROR
+ }
+
+ Map tags = ext.namedTags.collectEntries { taskName, tagName ->
+ [
+ generateTagTaskName(taskName),
+ [
+ tagName: tagName,
+ tagTask: {
+ -> tagName }
+ ]
+ ]
+ }
+
+ if (!ext.tags.isEmpty()) {
+ ext.tags.each { unresolvedTagName ->
+ String taskName = generateTagTaskName(unresolvedTagName)
+
+ if (tags.containsKey(taskName)) {
+ throw new IllegalArgumentException("Task name '${taskName}' is existed.")
+ }
+
+ tags[taskName] = [
+ tagName: unresolvedTagName,
+ tagTask: {
+ -> computeName(ext.name, unresolvedTagName) }
+ ]
+ }
+ }
+
+ tags.each { taskName, tagConfig ->
+ Exec tagSubTask = project.tasks.create('dockerTag' + taskName, Exec, {
+ group = 'Docker'
+ description = "Tags Docker image with tag '${tagConfig.tagName}'"
+ workingDir dockerDir
+ commandLine 'docker', 'tag', "${-> ext.name}", "${-> tagConfig.tagTask()}"
+ dependsOn exec
+ })
+ tag.dependsOn tagSubTask
+
+ Exec pushSubTask = project.tasks.create('dockerPush' + taskName, Exec, {
+ group = 'Docker'
+ description = "Pushes the Docker image with tag '${tagConfig.tagName}' to configured Docker Hub"
+ workingDir dockerDir
+ commandLine 'docker', 'push', "${-> tagConfig.tagTask()}"
+ dependsOn tagSubTask
+ })
+ pushAllTags.dependsOn pushSubTask
+ }
+ }
+ }
+
+ private List buildCommandLine(DockerExtension ext) {
+ List buildCommandLine = ['docker']
+ if (ext.buildx) {
+ buildCommandLine.addAll(['buildx', 'build'])
+ if (!ext.platform.isEmpty()) {
+ buildCommandLine.addAll('--platform', String.join(',', ext.platform))
+ }
+ if (ext.load) {
+ buildCommandLine.add '--load'
+ }
+ if (ext.push) {
+ buildCommandLine.add '--push'
+ if (ext.load) {
+ throw new Exception("cannot combine 'push' and 'load' options")
+ }
+ }
+ if (ext.builder != null) {
+ buildCommandLine.addAll('--builder', ext.builder)
+ }
+ } else {
+ buildCommandLine.add 'build'
+ }
+ if (ext.noCache) {
+ buildCommandLine.add '--no-cache'
+ }
+ if (ext.getNetwork() != null) {
+ buildCommandLine.addAll('--network', ext.network)
+ }
+ if (!ext.buildArgs.isEmpty()) {
+ for (Map.Entry buildArg : ext.buildArgs.entrySet()) {
+ buildCommandLine.addAll('--build-arg', "${buildArg.getKey()}=${buildArg.getValue()}" as String)
+ }
+ }
+ if (!ext.labels.isEmpty()) {
+ for (Map.Entry label : ext.labels.entrySet()) {
+ if (!label.getKey().matches(LABEL_KEY_PATTERN)) {
+ throw new GradleException(String.format("Docker label '%s' contains illegal characters. " +
+ "Label keys must only contain lowercase alphanumberic, `.`, or `-` characters (must match %s).",
+ label.getKey(), LABEL_KEY_PATTERN.pattern()))
+ }
+ buildCommandLine.addAll('--label', "${label.getKey()}=${label.getValue()}" as String)
+ }
+ }
+ if (ext.pull) {
+ buildCommandLine.add '--pull'
+ }
+ buildCommandLine.addAll(['-t', "${-> ext.name}", '.'])
+ logger.debug("${buildCommandLine}" as String)
+ return buildCommandLine
+ }
+
+ private static String computeName(String name, String tag) {
+ int firstAt = tag.indexOf("@")
+
+ String tagValue
+ if (firstAt > 0) {
+ tagValue = tag.substring(firstAt + 1, tag.length())
+ } else {
+ tagValue = tag
+ }
+
+ if (tagValue.contains(':') || tagValue.contains('/')) {
+ // tag with ':' or '/' -> force use the tag value
+ return tagValue
+ } else {
+ // tag without ':' and '/' -> replace the tag part of original name
+ int lastColon = name.lastIndexOf(':')
+ int lastSlash = name.lastIndexOf('/')
+
+ int endIndex;
+
+ // image_name -> this should remain
+ // host:port/image_name -> this should remain.
+ // host:port/image_name:v1 -> v1 should be replaced
+ if (lastColon > lastSlash) endIndex = lastColon
+ else endIndex = name.length()
+
+ return name.substring(0, endIndex) + ":" + tagValue
+ }
+ }
+
+ private static String generateTagTaskName(String name) {
+ String tagTaskName = name
+ int firstAt = name.indexOf("@")
+
+ if (firstAt > 0) {
+ // Get substring of task name
+ tagTaskName = name.substring(0, firstAt)
+ } else if (firstAt == 0) {
+ // Task name must not be empty
+ throw new GradleException("Task name of docker tag '${name}' must not be empty.")
+ } else if (name.contains(':') || name.contains('/')) {
+ // Tags which with repo or name must have a task name
+ throw new GradleException("Docker tag '${name}' must have a task name.")
+ }
+
+ StringBuffer sb = new StringBuffer(tagTaskName)
+ // Uppercase the first letter of task name
+ sb.replace(0, 1, tagTaskName.substring(0, 1).toUpperCase());
+ return sb.toString()
+ }
+}
diff --git a/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerRunPlugin.groovy b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerRunPlugin.groovy
new file mode 100644
index 000000000000..5297c7018139
--- /dev/null
+++ b/buildSrc/src/main/groovy/org/apache/beam/gradle/BeamDockerRunPlugin.groovy
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * License); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an AS IS BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.beam.gradle
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.tasks.Exec
+
+/**
+ * A gradle plug-in handling 'docker run' command. Originally replicated from
+ * com.palantir.docker-run plugin.
+ */
+class BeamDockerRunPlugin implements Plugin {
+
+ /** A class defining the configurations of dockerRun task. */
+ static class DockerRunExtension {
+ String name
+ String image
+ Set ports = [] as Set
+ Map env = [:]
+ List arguments = []
+ Map volumes = [:]
+ boolean daemonize = true
+ boolean clean = false
+
+ public String getName() {
+ return name
+ }
+
+ public void setName(String name) {
+ this.name = name
+ }
+ }
+
+ @Override
+ void apply(Project project) {
+ DockerRunExtension ext = project.extensions.create('dockerRun', DockerRunExtension)
+
+ Exec dockerRunStatus = project.tasks.create('dockerRunStatus', Exec, {
+ group = 'Docker Run'
+ description = 'Checks the run status of the container'
+ })
+
+ Exec dockerRun = project.tasks.create('dockerRun', Exec, {
+ group = 'Docker Run'
+ description = 'Runs the specified container with port mappings'
+ })
+
+ Exec dockerStop = project.tasks.create('dockerStop', Exec, {
+ group = 'Docker Run'
+ description = 'Stops the named container if it is running'
+ ignoreExitValue = true
+ })
+
+ Exec dockerRemoveContainer = project.tasks.create('dockerRemoveContainer', Exec, {
+ group = 'Docker Run'
+ description = 'Removes the persistent container associated with the Docker Run tasks'
+ ignoreExitValue = true
+ })
+
+ project.afterEvaluate {
+ /** Inspect status of docker. */
+ dockerRunStatus.with {
+ standardOutput = new ByteArrayOutputStream()
+ commandLine 'docker', 'inspect', '--format={{.State.Running}}', ext.name
+ doLast {
+ if (standardOutput.toString().trim() != 'true') {
+ println "Docker container '${ext.name}' is STOPPED."
+ return 1
+ } else {
+ println "Docker container '${ext.name}' is RUNNING."
+ }
+ }
+ }
+
+ /**
+ * Run a docker container. See {@link DockerRunExtension} for supported
+ * arguments.
+ *
+ * Replication of dockerRun task of com.palantir.docker-run plugin.
+ */
+ dockerRun.with {
+ List args = new ArrayList()
+ args.addAll(['docker', 'run'])
+
+ if (ext.daemonize) {
+ args.add('-d')
+ }
+ if (ext.clean) {
+ args.add('--rm')
+ } else {
+ finalizedBy dockerRunStatus
+ }
+ for (String port : ext.ports) {
+ args.add('-p')
+ args.add(port)
+ }
+ for (Map.Entry