diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/DockerBase.java b/buildSrc/src/main/java/org/elasticsearch/gradle/DockerBase.java new file mode 100644 index 000000000000..8257b30a287d --- /dev/null +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/DockerBase.java @@ -0,0 +1,39 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.gradle; + +/** + * This class models the different Docker base images that are used to build Docker distributions of Elasticsearch. + */ +public enum DockerBase { + CENTOS("centos:8"), + // "latest" here is intentional, since the image name specifies "8" + UBI("registry.access.redhat.com/ubi8/ubi-minimal:latest"); + + private final String image; + + DockerBase(String image) { + this.image = image; + } + + public String getImage() { + return image; + } +} diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/ElasticsearchDistribution.java b/buildSrc/src/main/java/org/elasticsearch/gradle/ElasticsearchDistribution.java index 8236426b69a2..697e31bba132 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/ElasticsearchDistribution.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/ElasticsearchDistribution.java @@ -50,7 +50,9 @@ public class ElasticsearchDistribution implements Buildable, Iterable { ARCHIVE, RPM, DEB, - DOCKER; + DOCKER, + // This is a different flavour of Docker image + DOCKER_UBI; @Override public String toString() { @@ -61,6 +63,7 @@ public class ElasticsearchDistribution implements Buildable, Iterable { switch (this) { case DEB: case DOCKER: + case DOCKER_UBI: case RPM: return false; @@ -188,6 +191,11 @@ public class ElasticsearchDistribution implements Buildable, Iterable { return bundledJdk.getOrElse(true); } + public boolean isDocker() { + final Type type = this.type.get(); + return type == Type.DOCKER || type == Type.DOCKER_UBI; + } + public void setBundledJdk(Boolean bundledJdk) { this.bundledJdk.set(bundledJdk); } @@ -230,9 +238,7 @@ public class ElasticsearchDistribution implements Buildable, Iterable { @Override public TaskDependency getBuildDependencies() { // For non-required Docker distributions, skip building the distribution is Docker is unavailable - if (getType() == Type.DOCKER - && getFailIfUnavailable() == false - && dockerSupport.get().getDockerAvailability().isAvailable == false) { + if (isDocker() && getFailIfUnavailable() == false && dockerSupport.get().getDockerAvailability().isAvailable == false) { return task -> Collections.emptySet(); } @@ -266,7 +272,7 @@ public class ElasticsearchDistribution implements Buildable, Iterable { return; } - if (getType() != Type.DOCKER && failIfUnavailable.get() == false) { + if (isDocker() == false && failIfUnavailable.get() == false) { throw new IllegalArgumentException( "failIfUnavailable cannot be 'false' on elasticsearch distribution [" + name + "] of type [" + getType() + "]" ); @@ -283,10 +289,15 @@ public class ElasticsearchDistribution implements Buildable, Iterable { "platform cannot be set on elasticsearch distribution [" + name + "] of type [" + getType() + "]" ); } - if (getType() == Type.DOCKER && bundledJdk.isPresent()) { - throw new IllegalArgumentException( - "bundledJdk cannot be set on elasticsearch distribution [" + name + "] of type [docker]" - ); + if (isDocker()) { + if (bundledJdk.isPresent()) { + throw new IllegalArgumentException( + "bundledJdk cannot be set on elasticsearch distribution [" + name + "] of type " + "[docker]" + ); + } + if (flavor.get() == Flavor.OSS && type.get() == Type.DOCKER_UBI) { + throw new IllegalArgumentException("Cannot build a UBI docker image for the OSS distribution"); + } } } diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java index 9399f1f062ca..9747e1a5ed1f 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/internal/InternalDistributionDownloadPlugin.java @@ -102,6 +102,7 @@ public class InternalDistributionDownloadPlugin implements Plugin { break; case DOCKER: + case DOCKER_UBI: projectPath += ":docker:"; projectPath += distributionProjectName(distribution); break; @@ -148,6 +149,10 @@ public class InternalDistributionDownloadPlugin implements Plugin { projectName += "docker" + archString + "-export"; break; + case DOCKER_UBI: + projectName += "ubi-docker" + archString + "-export"; + break; + default: projectName += distribution.getType(); break; diff --git a/buildSrc/src/main/java/org/elasticsearch/gradle/test/DistroTestPlugin.java b/buildSrc/src/main/java/org/elasticsearch/gradle/test/DistroTestPlugin.java index 6a6592fa0a6a..4fbff96d848e 100644 --- a/buildSrc/src/main/java/org/elasticsearch/gradle/test/DistroTestPlugin.java +++ b/buildSrc/src/main/java/org/elasticsearch/gradle/test/DistroTestPlugin.java @@ -107,7 +107,7 @@ public class DistroTestPlugin implements Plugin { depsTask.configure(t -> t.dependsOn(distribution, examplePlugin)); depsTasks.put(taskname, depsTask); TaskProvider destructiveTask = configureTestTask(project, taskname, distribution, t -> { - t.onlyIf(t2 -> distribution.getType() != Type.DOCKER || dockerSupport.get().getDockerAvailability().isAvailable); + t.onlyIf(t2 -> distribution.isDocker() == false || dockerSupport.get().getDockerAvailability().isAvailable); addDistributionSysprop(t, DISTRIBUTION_SYSPROP, distribution::toString); addDistributionSysprop(t, EXAMPLE_PLUGIN_SYSPROP, () -> examplePlugin.getSingleFile().toString()); t.exclude("**/PackageUpgradeTests.class"); @@ -191,7 +191,8 @@ public class DistroTestPlugin implements Plugin { // auto-detection doesn't work. // // The shouldTestDocker property could be null, hence we use Boolean.TRUE.equals() - boolean shouldExecute = type != Type.DOCKER || Boolean.TRUE.equals(vmProject.findProperty("shouldTestDocker")); + boolean shouldExecute = (type != Type.DOCKER && type != Type.DOCKER_UBI) + || Boolean.TRUE.equals(vmProject.findProperty("shouldTestDocker")); if (shouldExecute) { distroTest.configure(t -> t.dependsOn(wrapperTask)); @@ -218,6 +219,7 @@ public class DistroTestPlugin implements Plugin { Map> lifecyleTasks = new HashMap<>(); lifecyleTasks.put(Type.DOCKER, project.getTasks().register(taskPrefix + ".docker")); + lifecyleTasks.put(Type.DOCKER_UBI, project.getTasks().register(taskPrefix + ".ubi")); lifecyleTasks.put(Type.ARCHIVE, project.getTasks().register(taskPrefix + ".archives")); lifecyleTasks.put(Type.DEB, project.getTasks().register(taskPrefix + ".packages")); lifecyleTasks.put(Type.RPM, lifecyleTasks.get(Type.DEB)); @@ -344,26 +346,28 @@ public class DistroTestPlugin implements Plugin { List currentDistros = new ArrayList<>(); for (Architecture architecture : Architecture.values()) { - for (Type type : List.of(Type.DEB, Type.RPM, Type.DOCKER)) { + for (Type type : List.of(Type.DEB, Type.RPM, Type.DOCKER, Type.DOCKER_UBI)) { for (Flavor flavor : Flavor.values()) { for (boolean bundledJdk : Arrays.asList(true, false)) { - // All our Docker images include a bundled JDK so it doesn't make sense to test without one. - // Also we'll never publish an ARM (aarch64) build without a bundled JDK. - boolean skip = bundledJdk == false && (type == Type.DOCKER || architecture == Architecture.AARCH64); - - if (skip == false) { - currentDistros.add( - createDistro( - distributions, - architecture, - type, - null, - flavor, - bundledJdk, - VersionProperties.getElasticsearch() - ) - ); + if (bundledJdk == false) { + // We'll never publish an ARM (aarch64) build without a bundled JDK. + if (architecture == Architecture.AARCH64) { + continue; + } + // All our Docker images include a bundled JDK so it doesn't make sense to test without one. + if (type == Type.DOCKER || type == Type.DOCKER_UBI) { + continue; + } } + + // We don't publish the OSS distribution on UBI + if (type == Type.DOCKER_UBI && flavor == Flavor.OSS) { + continue; + } + + currentDistros.add( + createDistro(distributions, architecture, type, null, flavor, bundledJdk, VersionProperties.getElasticsearch()) + ); } } } @@ -408,6 +412,7 @@ public class DistroTestPlugin implements Plugin { String version ) { String name = distroId(type, platform, flavor, bundledJdk, architecture) + "-" + version; + boolean isDocker = type == Type.DOCKER || type == Type.DOCKER_UBI; ElasticsearchDistribution distro = distributions.create(name, d -> { d.setArchitecture(architecture); d.setFlavor(flavor); @@ -415,7 +420,7 @@ public class DistroTestPlugin implements Plugin { if (type == Type.ARCHIVE) { d.setPlatform(platform); } - if (type != Type.DOCKER) { + if (isDocker == false) { d.setBundledJdk(bundledJdk); } d.setVersion(version); @@ -423,7 +428,7 @@ public class DistroTestPlugin implements Plugin { // Allow us to gracefully omit building Docker distributions if Docker is not available on the system. // In such a case as we can't build the Docker images we'll simply skip the corresponding tests. - if (type == Type.DOCKER) { + if (isDocker) { distro.setFailIfUnavailable(false); } diff --git a/distribution/docker/build.gradle b/distribution/docker/build.gradle index 697543a1d5f0..a0e93c7977c5 100644 --- a/distribution/docker/build.gradle +++ b/distribution/docker/build.gradle @@ -1,4 +1,5 @@ import org.elasticsearch.gradle.Architecture +import org.elasticsearch.gradle.DockerBase import org.elasticsearch.gradle.ElasticsearchDistribution.Flavor import org.elasticsearch.gradle.LoggedExec import org.elasticsearch.gradle.VersionProperties @@ -28,18 +29,15 @@ dependencies { ossDockerSource project(path: ":distribution:archives:oss-linux-tar") } -ext.expansions = { architecture, oss, local -> +ext.expansions = { Architecture architecture, boolean oss, DockerBase base, boolean local -> String classifier if (local) { - switch (architecture) { - case "aarch64": - classifier = "linux-aarch64" - break - case "x64": - classifier = "linux-x86_64" - break - default: - throw new IllegalArgumentException("Unrecognized architecture [" + architecture + "], must be one of (aarch64|x64)") + if (architecture == Architecture.AARCH64) { + classifier = "linux-aarch64" + } else if (architecture == Architecture.X64) { + classifier = "linux-x86_64" + } else { + throw new IllegalArgumentException("Unsupported architecture [" + architecture + "]") } } else { /* When sourcing the Elasticsearch build remotely, the same Dockerfile needs @@ -66,24 +64,35 @@ RUN curl --retry 8 -S -L \\ } return [ + 'base_image' : base.getImage(), 'build_date' : BuildParams.buildDate, 'git_revision' : BuildParams.gitRevision, 'license' : oss ? 'Apache-2.0' : 'Elastic-License', + 'package_manager' : base == DockerBase.UBI ? 'microdnf' : 'yum', 'source_elasticsearch': sourceElasticsearch, + 'docker_base' : base.name().toLowerCase(), 'version' : VersionProperties.elasticsearch ] } -private static String buildPath(final String architecture, final boolean oss) { - return "build/${"aarch64".equals(architecture) ? 'aarch64-' : ''}${oss ? 'oss-' : ''}docker" +private static String buildPath(Architecture architecture, boolean oss, DockerBase base) { + return 'build/' + + (architecture == Architecture.AARCH64 ? 'aarch64-' : '') + + (oss ? 'oss-' : '') + + (base == DockerBase.UBI ? 'ubi-' : '') + + 'docker' } -private static String taskName(final String prefix, final String architecture, final boolean oss, final String suffix) { - return "${prefix}${"aarch64".equals(architecture) ? 'Aarch64' : ''}${oss ? 'Oss' : ''}${suffix}" +private static String taskName(String prefix, Architecture architecture, boolean oss, DockerBase base, String suffix) { + return prefix + + (architecture == Architecture.AARCH64 ? 'Aarch64' : '') + + (oss ? 'Oss' : '') + + (base == DockerBase.UBI ? 'Ubi' : '') + + suffix } project.ext { - dockerBuildContext = { String architecture, boolean oss, boolean local -> + dockerBuildContext = { Architecture architecture, boolean oss, DockerBase base, boolean local -> copySpec { into('bin') { from project.projectDir.toPath().resolve("src/docker/bin") @@ -91,33 +100,40 @@ project.ext { into('config') { /* - * Oss and default distribution can have different configuration, therefore we want to allow overriding the default configuration - * by creating config files in oss or default build-context sub-modules. + * The OSS and default distributions have different configurations, therefore we want to allow overriding the default configuration + * from files in the 'oss' sub-directory. We don't want the 'oss' sub-directory to appear in the final build context, however. */ - duplicatesStrategy = DuplicatesStrategy.INCLUDE - from project.projectDir.toPath().resolve("src/docker/config") + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(project.projectDir.toPath().resolve("src/docker/config")) { + exclude 'oss' + } if (oss) { + // Overlay the config file from project.projectDir.toPath().resolve("src/docker/config/oss") } } from(project.projectDir.toPath().resolve("src/docker/Dockerfile")) { - expand(expansions(architecture, oss, local)) + expand(expansions(architecture, oss, base, local)) } } } } -void addCopyDockerContextTask(final String architecture, final boolean oss) { - tasks.register(taskName("copy", architecture, oss, "DockerContext"), Sync) { - expansions(architecture, oss, true).findAll { it.key != 'build_date' }.each { k, v -> +void addCopyDockerContextTask(Architecture architecture, boolean oss, DockerBase base) { + if (oss && base != DockerBase.CENTOS) { + throw new GradleException("The only allowed docker base image for OSS builds is CENTOS") + } + + tasks.register(taskName("copy", architecture, oss, base, "DockerContext"), Sync) { + expansions(architecture, oss, base, true).findAll { it.key != 'build_date' }.each { k, v -> inputs.property(k, { v.toString() }) } - into buildPath(architecture, oss) + into buildPath(architecture, oss, base) - with dockerBuildContext(architecture, oss, true) + with dockerBuildContext(architecture, oss, base, true) - if ("aarch64".equals(architecture)) { + if (architecture == Architecture.AARCH64) { if (oss) { from configurations.aarch64OssDockerSource } else { @@ -202,25 +218,31 @@ tasks.named("check").configure { dependsOn "integTest" } -void addBuildDockerImage(final String architecture, final boolean oss) { +void addBuildDockerImage(Architecture architecture, boolean oss, DockerBase base) { + if (oss && base != DockerBase.CENTOS) { + throw new GradleException("The only allowed docker base image for OSS builds is CENTOS") + } + final TaskProvider buildDockerImageTask = - tasks.register(taskName("build", architecture, oss, "DockerImage"), DockerBuildTask) { - onlyIf { Architecture.current().name().toLowerCase().equals(architecture) } - TaskProvider copyContextTask = tasks.named(taskName("copy", architecture, oss, "DockerContext")) + tasks.register(taskName("build", architecture, oss, base, "DockerImage"), DockerBuildTask) { + onlyIf { Architecture.current() == architecture } + TaskProvider copyContextTask = tasks.named(taskName("copy", architecture, oss, base, "DockerContext")) dependsOn(copyContextTask) dockerContext.fileProvider(copyContextTask.map { it.destinationDir }) + String version = VersionProperties.elasticsearch if (oss) { tags = [ - "docker.elastic.co/elasticsearch/elasticsearch-oss:${VersionProperties.elasticsearch}", + "docker.elastic.co/elasticsearch/elasticsearch-oss:${version}", "elasticsearch-oss:test" ] } else { + String suffix = base == DockerBase.UBI ? '-ubi8' : '' tags = [ - "elasticsearch:${VersionProperties.elasticsearch}", - "docker.elastic.co/elasticsearch/elasticsearch:${VersionProperties.elasticsearch}", - "docker.elastic.co/elasticsearch/elasticsearch-full:${VersionProperties.elasticsearch}", - "elasticsearch:test", + "elasticsearch${suffix}:${version}", + "docker.elastic.co/elasticsearch/elasticsearch${suffix}:${version}", + "docker.elastic.co/elasticsearch/elasticsearch-full${suffix}:${version}", + "elasticsearch${suffix}:test", ] } } @@ -229,10 +251,17 @@ void addBuildDockerImage(final String architecture, final boolean oss) { } } -for (final String architecture : ["aarch64", "x64"]) { - for (final boolean oss : [false, true]) { - addCopyDockerContextTask(architecture, oss) - addBuildDockerImage(architecture, oss) +for (final Architecture architecture : Architecture.values()) { + for (final DockerBase base : DockerBase.values()) { + for (final boolean oss : [false, true]) { + if (oss && base != DockerBase.CENTOS) { + // We only create Docker images for the OSS distribution on CentOS. + // Other bases only use the default distribution. + continue + } + addCopyDockerContextTask(architecture, oss, base) + addBuildDockerImage(architecture, oss, base) + } } } @@ -245,35 +274,41 @@ if (tasks.findByName("composePull")) { /* * The export subprojects write out the generated Docker images to disk, so - * that they can be easily reloaded, for example into a VM. + * that they can be easily reloaded, for example into a VM for distribution testing */ subprojects { Project subProject -> if (subProject.name.endsWith('-export')) { apply plugin: 'distribution' - final String architecture = subProject.name.contains('aarch64-') ? 'aarch64' : 'x64' + final Architecture architecture = subProject.name.contains('aarch64-') ? Architecture.AARCH64 : Architecture.X64 final boolean oss = subProject.name.contains('oss-') + final DockerBase base = subProject.name.contains('ubi-') ? DockerBase.UBI : DockerBase.CENTOS - def exportTaskName = taskName("export", architecture, oss, "DockerImage") - def buildTaskName = taskName("build", architecture, oss, "DockerImage") - def tarFile = "${parent.projectDir}/build/elasticsearch${"aarch64".equals(architecture) ? '-aarch64' : ''}${oss ? '-oss' : ''}_test.${VersionProperties.elasticsearch}.docker.tar" + final String arch = architecture == Architecture.AARCH64 ? '-aarch64' : '' + final String suffix = oss ? '-oss' : base == DockerBase.UBI ? '-ubi8' : '' + final String extension = base == DockerBase.UBI ? 'ubi.tar' : 'docker.tar' + final String artifactName = "elasticsearch${arch}${suffix}_test" - final TaskProvider exportDockerImageTask = tasks.register(exportTaskName, LoggedExec) { + final String exportTaskName = taskName("export", architecture, oss, base, "DockerImage") + final String buildTaskName = taskName("build", architecture, oss, base, "DockerImage") + final String tarFile = "${parent.projectDir}/build/${artifactName}_${VersionProperties.elasticsearch}.${extension}" + + tasks.register(exportTaskName, LoggedExec) { inputs.file("${parent.projectDir}/build/markers/${buildTaskName}.marker") executable 'docker' outputs.file(tarFile) args "save", "-o", tarFile, - "elasticsearch${oss ? '-oss' : ''}:test" + "elasticsearch${suffix}:test" dependsOn(parent.path + ":" + buildTaskName) - onlyIf { Architecture.current().name().toLowerCase().equals(architecture) } + onlyIf { Architecture.current() == architecture } } artifacts.add('default', file(tarFile)) { type 'tar' - name "elasticsearch${"aarch64".equals(architecture) ? '-aarch64' : ''}${oss ? '-oss' : ''}" + name artifactName builtBy exportTaskName } diff --git a/distribution/docker/docker-build-context/build.gradle b/distribution/docker/docker-build-context/build.gradle index dc61d46bfd2b..0f5a9e5d6773 100644 --- a/distribution/docker/docker-build-context/build.gradle +++ b/distribution/docker/docker-build-context/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.DockerBase + apply plugin: 'base' tasks.register("buildDockerBuildContext", Tar) { @@ -6,8 +8,7 @@ tasks.register("buildDockerBuildContext", Tar) { archiveClassifier = "docker-build-context" archiveBaseName = "elasticsearch" // Non-local builds don't need to specify an architecture. - // Make this explicit via the string value. - with dockerBuildContext("", false, false) + with dockerBuildContext(null, false, DockerBase.CENTOS, false) } tasks.named("assemble").configure {dependsOn "buildDockerBuildContext"} diff --git a/distribution/docker/oss-docker-build-context/build.gradle b/distribution/docker/oss-docker-build-context/build.gradle index 819d3813acec..54fa932652d2 100644 --- a/distribution/docker/oss-docker-build-context/build.gradle +++ b/distribution/docker/oss-docker-build-context/build.gradle @@ -1,3 +1,5 @@ +import org.elasticsearch.gradle.DockerBase + apply plugin: 'base' tasks.register("buildOssDockerBuildContext", Tar) { @@ -6,8 +8,7 @@ tasks.register("buildOssDockerBuildContext", Tar) { archiveClassifier = "docker-build-context" archiveBaseName = "elasticsearch-oss" // Non-local builds don't need to specify an architecture. - // Make this explicit via the string value. - with dockerBuildContext("", true, false) + with dockerBuildContext(null, true, DockerBase.CENTOS, false) } tasks.named("assemble").configure { dependsOn "buildOssDockerBuildContext" } diff --git a/distribution/docker/src/docker/Dockerfile b/distribution/docker/src/docker/Dockerfile index 961eff82ed76..e5b04ffe91b2 100644 --- a/distribution/docker/src/docker/Dockerfile +++ b/distribution/docker/src/docker/Dockerfile @@ -9,15 +9,21 @@ can also comment out blocks, like this one. See: https://docs.groovy-lang.org/latest/html/api/groovy/text/SimpleTemplateEngine.html + + We use control-flow tags in this file to conditionally render the content. The + layout/presentation here has been adjusted so that it looks reasonable when rendered, + at the slight expense of how it looks here. */ %> ################################################################################ # Build stage 0 `builder`: -# Extract elasticsearch artifact -# Set gid=0 and make group perms==owner perms +# Extract Elasticsearch artifact ################################################################################ -FROM centos:8 AS builder - +FROM ${base_image} AS builder +<% if (docker_base == 'ubi') { %> +# Install required packages to extract the Elasticsearch distribution +RUN ${package_manager} install -y tar gzip +<% } %> # `tini` is a tiny but valid init for containers. This is used to cleanly # control how ES and any child processes are shut down. # @@ -25,7 +31,6 @@ FROM centos:8 AS builder # gpg, but the keyservers are slow to return the key and this can fail the # build. Instead, we check the binary against the published checksum. RUN set -eux ; \\ - \\ tini_bin="" ; \\ case "\$(arch)" in \\ aarch64) tini_bin='tini-arm64' ;; \\ @@ -39,11 +44,7 @@ RUN set -eux ; \\ mv \${tini_bin} /tini ; \\ chmod +x /tini -ENV PATH /usr/share/elasticsearch/bin:\$PATH - -RUN groupadd -g 1000 elasticsearch && \\ - adduser -u 1000 -g 1000 -d /usr/share/elasticsearch elasticsearch - +RUN mkdir /usr/share/elasticsearch WORKDIR /usr/share/elasticsearch ${source_elasticsearch} @@ -56,29 +57,33 @@ COPY config/elasticsearch.yml config/log4j2.properties config/ RUN chmod 0660 config/elasticsearch.yml config/log4j2.properties ################################################################################ -# Build stage 1 (the actual elasticsearch image): +# Build stage 1 (the actual Elasticsearch image): +# # Copy elasticsearch from stage 0 # Add entrypoint ################################################################################ -FROM centos:8 +FROM ${base_image} ENV ELASTIC_CONTAINER true -COPY --from=builder /tini /tini - -RUN for iter in {1..10}; do yum update --setopt=tsflags=nodocs -y && \\ - yum install --setopt=tsflags=nodocs -y nc shadow-utils zip unzip && \\ - yum clean all && exit_code=0 && break || exit_code=\$? && echo "yum error: retry \$iter in 10s" && sleep 10; done; \\ +RUN for iter in {1..10}; do \\ + ${package_manager} update --setopt=tsflags=nodocs -y && \\ + ${package_manager} install --setopt=tsflags=nodocs -y \\ + nc shadow-utils zip unzip <%= docker_base == 'ubi' ? 'findutils procps-ng' : '' %> && \\ + ${package_manager} clean all && exit_code=0 && break || exit_code=\$? && echo "${package_manager} error: retry \$iter in 10s" && \\ + sleep 10; \\ + done; \\ (exit \$exit_code) RUN groupadd -g 1000 elasticsearch && \\ adduser -u 1000 -g 1000 -G 0 -d /usr/share/elasticsearch elasticsearch && \\ chmod 0775 /usr/share/elasticsearch && \\ - chgrp 0 /usr/share/elasticsearch + chown -R 1000:0 /usr/share/elasticsearch WORKDIR /usr/share/elasticsearch COPY --from=builder --chown=1000:0 /usr/share/elasticsearch /usr/share/elasticsearch +COPY --from=builder --chown=0:0 /tini /tini # Replace OpenJDK's built-in CA certificate keystore with the one from the OS # vendor. The latter is superior in several ways. @@ -116,7 +121,18 @@ LABEL org.label-schema.build-date="${build_date}" \\ org.opencontainers.image.url="https://www.elastic.co/products/elasticsearch" \\ org.opencontainers.image.vendor="Elastic" \\ org.opencontainers.image.version="${version}" +<% if (docker_base == 'ubi') { %> +LABEL name="Elasticsearch" \\ + maintainer="infra@elastic.co" \\ + vendor="Elastic" \\ + version="${version}" \\ + release="1" \\ + summary="Elasticsearch" \\ + description="You know, for search." +RUN mkdir /licenses && \\ + cp LICENSE.txt /licenses/LICENSE +<% } %> USER elasticsearch:root ENTRYPOINT ["/tini", "--", "/usr/local/bin/docker-entrypoint.sh"] diff --git a/distribution/docker/ubi-docker-aarch64-export/build.gradle b/distribution/docker/ubi-docker-aarch64-export/build.gradle new file mode 100644 index 000000000000..537b5a093683 --- /dev/null +++ b/distribution/docker/ubi-docker-aarch64-export/build.gradle @@ -0,0 +1,2 @@ +// This file is intentionally blank. All configuration of the +// export is done in the parent project. diff --git a/distribution/docker/ubi-docker-build-context/build.gradle b/distribution/docker/ubi-docker-build-context/build.gradle new file mode 100644 index 000000000000..3fb9ba3a2b42 --- /dev/null +++ b/distribution/docker/ubi-docker-build-context/build.gradle @@ -0,0 +1,13 @@ +import org.elasticsearch.gradle.DockerBase + +apply plugin: 'base' + +task buildUbiDockerBuildContext(type: Tar) { + archiveExtension = 'tar.gz' + compression = Compression.GZIP + archiveClassifier = "docker-build-context" + archiveBaseName = "elasticsearch-ubi8" + with dockerBuildContext(null, false, DockerBase.UBI, false) +} + +assemble.dependsOn buildUbiDockerBuildContext diff --git a/distribution/docker/ubi-docker-export/build.gradle b/distribution/docker/ubi-docker-export/build.gradle new file mode 100644 index 000000000000..537b5a093683 --- /dev/null +++ b/distribution/docker/ubi-docker-export/build.gradle @@ -0,0 +1,2 @@ +// This file is intentionally blank. All configuration of the +// export is done in the parent project. diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java index 0bdd8a7be7f5..cccd00bb3cfd 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/DockerTests.java @@ -21,6 +21,7 @@ package org.elasticsearch.packaging.test; import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.client.fluent.Request; +import org.elasticsearch.packaging.util.Distribution; import org.elasticsearch.packaging.util.Installation; import org.elasticsearch.packaging.util.Platforms; import org.elasticsearch.packaging.util.ServerUtils; @@ -56,6 +57,7 @@ import static org.elasticsearch.packaging.util.FileMatcher.p660; import static org.elasticsearch.packaging.util.FileMatcher.p775; import static org.elasticsearch.packaging.util.FileUtils.append; import static org.elasticsearch.packaging.util.FileUtils.rm; +import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.arrayWithSize; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.emptyString; @@ -622,4 +624,44 @@ public class DockerTests extends PackagingTestCase { assertThat("Failed to find [cpu] in node OS cgroup stats", cgroupStats.get("cpu"), not(nullValue())); assertThat("Failed to find [cpuacct] in node OS cgroup stats", cgroupStats.get("cpuacct"), not(nullValue())); } + + /** + * Check that the UBI images has the correct license information in the correct place. + */ + public void test200UbiImagesHaveLicenseDirectory() { + assumeTrue(distribution.packaging == Distribution.Packaging.DOCKER_UBI); + + final String[] files = sh.run("find /licenses -type f").stdout.split("\n"); + assertThat(files, arrayContaining("/licenses/LICENSE")); + + // UBI image doesn't contain `diff` + final String ubiLicense = sh.run("cat /licenses/LICENSE").stdout; + final String distroLicense = sh.run("cat /usr/share/elasticsearch/LICENSE.txt").stdout; + assertThat(ubiLicense, equalTo(distroLicense)); + } + + /** + * Check that the UBI image has the expected labels + */ + public void test210UbiLabels() throws Exception { + assumeTrue(distribution.packaging == Distribution.Packaging.DOCKER_UBI); + + final Map labels = getImageLabels(distribution); + + final Map staticLabels = new HashMap<>(); + staticLabels.put("name", "Elasticsearch"); + staticLabels.put("maintainer", "infra@elastic.co"); + staticLabels.put("vendor", "Elastic"); + staticLabels.put("summary", "Elasticsearch"); + staticLabels.put("description", "You know, for search."); + + final Set dynamicLabels = Set.of("release", "version"); + + staticLabels.forEach((key, value) -> { + assertThat(labels, hasKey(key)); + assertThat(labels.get(key), equalTo(value)); + }); + + dynamicLabels.forEach(key -> assertThat(labels, hasKey(key))); + } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java index d4e3dc79fe7b..992335c9c2e1 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Distribution.java @@ -39,6 +39,8 @@ public class Distribution { this.packaging = Packaging.TAR; } else if (filename.endsWith(".docker.tar")) { this.packaging = Packaging.DOCKER; + } else if (filename.endsWith(".ubi.tar")) { + this.packaging = Packaging.DOCKER_UBI; } else { int lastDot = filename.lastIndexOf('.'); this.packaging = Packaging.valueOf(filename.substring(lastDot + 1).toUpperCase(Locale.ROOT)); @@ -71,7 +73,7 @@ public class Distribution { } public boolean isDocker() { - return packaging == Packaging.DOCKER; + return packaging == Packaging.DOCKER || packaging == Packaging.DOCKER_UBI; } public enum Packaging { @@ -80,7 +82,8 @@ public class Distribution { ZIP(".zip", Platforms.WINDOWS), DEB(".deb", Platforms.isDPKG()), RPM(".rpm", Platforms.isRPM()), - DOCKER(".docker.tar", Platforms.isDocker()); + DOCKER(".docker.tar", Platforms.isDocker()), + DOCKER_UBI(".ubi.tar", Platforms.isDocker()); /** The extension of this distribution's file */ public final String extension; diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java index 0c749c2e3517..169743bf4788 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/Docker.java @@ -77,7 +77,7 @@ public class Docker { * @param distribution details about the docker image to potentially load. */ public static void ensureImageIsLoaded(Distribution distribution) { - final long count = sh.run("docker image ls --format '{{.Repository}}' " + distribution.flavor.name).stdout.lines().count(); + final long count = sh.run("docker image ls --format '{{.Repository}}' " + getImageName(distribution)).stdout.lines().count(); if (count != 0) { return; @@ -168,7 +168,7 @@ public class Docker { } // Image name - args.add(distribution.flavor.name + ":test"); + args.add(getImageName(distribution)); final String command = String.join(" ", args); logger.info("Running command: " + command); @@ -563,7 +563,7 @@ public class Docker { public static Map getImageLabels(Distribution distribution) throws Exception { // The format below extracts the .Config.Labels value, and prints it as json. Without the json // modifier, a stringified Go map is printed instead, which isn't helpful. - String labelsJson = sh.run("docker inspect -f '{{json .Config.Labels}}' " + distribution.flavor.name + ":test").stdout; + String labelsJson = sh.run("docker inspect -f '{{json .Config.Labels}}' " + getImageName(distribution)).stdout; ObjectMapper mapper = new ObjectMapper(); @@ -579,4 +579,8 @@ public class Docker { public static Shell.Result getContainerLogs() { return sh.run("docker logs " + containerId); } + + private static String getImageName(Distribution distribution) { + return distribution.flavor.name + (distribution.packaging == Distribution.Packaging.DOCKER_UBI ? "-ubi8" : "") + ":test"; + } } diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java index 6b6cbf27b512..426461e9fdd6 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java @@ -71,7 +71,7 @@ public class ServerUtils { boolean xpackEnabled = false; // TODO: need a way to check if docker has security enabled, the yml config is not bind mounted so can't look from here - if (installation.distribution.packaging != Distribution.Packaging.DOCKER) { + if (installation.distribution.isDocker() == false) { Path configFilePath = installation.config("elasticsearch.yml"); // this is fragile, but currently doesn't deviate from a single line enablement and not worth the parsing effort String configFile = Files.readString(configFilePath, StandardCharsets.UTF_8); diff --git a/settings.gradle b/settings.gradle index 4a8bc1f9e426..eb484fcf3843 100644 --- a/settings.gradle +++ b/settings.gradle @@ -36,12 +36,15 @@ List projects = [ 'distribution:docker', 'distribution:docker:docker-aarch64-build-context', 'distribution:docker:docker-aarch64-export', - 'distribution:docker:oss-docker-aarch64-build-context', - 'distribution:docker:oss-docker-aarch64-export', 'distribution:docker:docker-build-context', 'distribution:docker:docker-export', + 'distribution:docker:oss-docker-aarch64-build-context', + 'distribution:docker:oss-docker-aarch64-export', 'distribution:docker:oss-docker-build-context', 'distribution:docker:oss-docker-export', + 'distribution:docker:ubi-docker-aarch64-export', + 'distribution:docker:ubi-docker-build-context', + 'distribution:docker:ubi-docker-export', 'distribution:packages:aarch64-oss-deb', 'distribution:packages:oss-deb', 'distribution:packages:aarch64-deb',