Skip to content

Commit

Permalink
feat(jib): multi-platform build support
Browse files Browse the repository at this point in the history
Signed-off-by: Marc Nuri <[email protected]>
  • Loading branch information
manusa committed May 28, 2024
1 parent f4d036a commit cd64fe7
Show file tree
Hide file tree
Showing 15 changed files with 549 additions and 162 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Usage:
./scripts/extract-changelog-for-version.sh 1.3.37 5
```
### 1.17-SNAPSHOT
* Fix #2098: Add support for multi-platform container image builds in jib build strategy
* Fix #2335: Add support for configuring nodeSelector spec for controller via xml/groovy DSL configuration
* Fix #2459: Allow configuring Buildpacks build via ImageConfiguration
* Fix #2462: `k8s:debug` throws error when using `buildpacks` build strategy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public enum ConfigKey {
LABELS(ValueCombinePolicy.MERGE),
MAINTAINER,
NAME,
PLATFORMS(ValueCombinePolicy.MERGE),
PORTS(ValueCombinePolicy.MERGE),
REGISTRY,
SHELL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ private BuildConfiguration extractBuildConfiguration(ImageConfiguration fromConf
.fromExt(valueProvider.getMap(ConfigKey.FROM_EXT, valueOrNull(config, BuildConfiguration::getFromExt)))
.clearVolumes().volumes(valueProvider.getList(ConfigKey.VOLUMES, valueOr(config, BuildConfiguration::getVolumes, Collections.emptyList())))
.clearTags().tags(valueProvider.getList(ConfigKey.TAGS, valueOr(config, BuildConfiguration::getTags, Collections.emptyList())))
.clearPlatforms().platforms(valueProvider.getList(ConfigKey.PLATFORMS, valueOr(config, BuildConfiguration::getPlatforms, Collections.emptyList())))
.maintainer(valueProvider.getString(ConfigKey.MAINTAINER, valueOrNull(config, BuildConfiguration::getMaintainer)))
.workdir(valueProvider.getString(ConfigKey.WORKDIR, valueOrNull(config, BuildConfiguration::getWorkdir)))
.skip(valueProvider.getBoolean(ConfigKey.SKIP, valueOrNull(config, BuildConfiguration::getSkip)))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ void setUp() {
.port("9082")
.tag("initial-tag-1")
.tag("initial-tag-2")
.platform("darwin/amd64")
.healthCheck(HealthCheckConfiguration.builder()
.interval("30s")
.build())
Expand All @@ -73,6 +74,8 @@ void setUp() {
javaProject.getProperties().put("jkube.container-image.ports.2", "9080");
javaProject.getProperties().put("jkube.container-image.tags.1", "tag-1");
javaProject.getProperties().put("jkube.container-image.tags.2", "tag-2");
javaProject.getProperties().put("jkube.container-image.platforms.1", "linux/amd64");
javaProject.getProperties().put("jkube.container-image.platforms.2", "linux/arm64");
javaProject.getProperties().put("jkube.container-image.healthcheck.interval", "10s");
}

Expand Down Expand Up @@ -149,11 +152,17 @@ void setsPorts() {
assertThat(resolved.getBuild().getPorts()).containsExactlyInAnyOrder("8080", "9080");
}


@Test
void setsTags() {
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("tag-1", "tag-2");
}

@Test
void setsPlatforms() {
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("linux/amd64", "linux/arm64");
}

@Test
void setsHealthCheckInterval() {
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("10s");
Expand Down Expand Up @@ -225,6 +234,11 @@ void appendsTags() {
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("tag-1", "tag-2", "initial-tag-1", "initial-tag-2");
}

@Test
void appendsPlatforms() {
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("linux/amd64", "linux/arm64", "darwin/amd64");
}

@Test
void overridesHealthCheckInterval() {
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("10s");
Expand Down Expand Up @@ -292,6 +306,11 @@ void preservesTags() {
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("initial-tag-1", "initial-tag-2");
}

@Test
void preservesPlatforms() {
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("darwin/amd64");
}

@Test
void preservesHealthCheckInterval() {
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("30s");
Expand Down
8 changes: 4 additions & 4 deletions jkube-kit/build/service/jib/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
<artifactId>junit-jupiter-params</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@
import com.google.cloud.tools.jib.api.CacheDirectoryCreationException;
import com.google.cloud.tools.jib.api.Containerizer;
import com.google.cloud.tools.jib.api.Credential;
import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
import com.google.cloud.tools.jib.api.Jib;
import com.google.cloud.tools.jib.api.JibContainerBuilder;
import com.google.cloud.tools.jib.api.LogEvent;
import com.google.cloud.tools.jib.api.RegistryException;
import com.google.cloud.tools.jib.api.TarImage;
import com.google.cloud.tools.jib.api.buildplan.Platform;
import com.google.cloud.tools.jib.event.events.ProgressEvent;
import org.eclipse.jkube.kit.build.api.assembly.AssemblyManager;
import org.eclipse.jkube.kit.build.api.assembly.BuildDirs;
Expand All @@ -39,8 +39,11 @@
import java.io.File;
import java.io.IOException;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
Expand All @@ -49,6 +52,8 @@
import static org.eclipse.jkube.kit.build.api.helper.RegistryUtil.getApplicablePullRegistryFrom;
import static org.eclipse.jkube.kit.build.api.helper.RegistryUtil.getApplicablePushRegistryFrom;
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.containerFromImageConfiguration;
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.platforms;
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.toImageReference;
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.toRegistryImage;

public class JibService implements AutoCloseable {
Expand Down Expand Up @@ -89,23 +94,33 @@ public final ImageName getImageName() {
/**
* Builds a container Jib container image tarball.
*
* @return the location of the generated tarball file.
* @return the location of the generated tarball files.
*/
public final File build() {
final JibContainerBuilder from = assembleFrom();
try {
final File jibImageTarArchive = getJibImageTarArchive();
final Containerizer to = Containerizer
.to(TarImage.at(jibImageTarArchive.toPath()).named(imageConfiguration.getName()));
public final List<File> build() {
final List<File> generatedTarballs = new ArrayList<>();
for (Platform platform : platforms(imageConfiguration)) {
final JibContainerBuilder from = assembleFrom();
from.setPlatforms(Collections.singleton(platform));
final File jibImageTarArchive = getJibImageTarArchive(platform);
final Containerizer to = Containerizer.to(
TarImage.at(jibImageTarArchive.toPath())
.named(toImageReference(imageConfiguration))
);
containerize(from, to);
return jibImageTarArchive;
} catch (InvalidImageReferenceException ex) {
throw new JKubeException("Unable to build the image tarball: " + ex.getMessage(), ex);
generatedTarballs.add(jibImageTarArchive);
}
return generatedTarballs;
}

public final void push() {
final JibContainerBuilder from = Jib.from(TarImage.at(getJibImageTarArchive().toPath()));
final Set<Platform> platforms = platforms(imageConfiguration);
final JibContainerBuilder from;
if (platforms.size() > 1) {
from = assembleFrom();
from.setPlatforms(platforms);
} else {
from = Jib.from(TarImage.at(getJibImageTarArchive(platforms.iterator().next()).toPath()));
}
final Containerizer to = Containerizer
.to(toRegistryImage(getImageName().getFullName(), getPushRegistryCredentials()));
containerize(from, to);
Expand Down Expand Up @@ -158,9 +173,10 @@ private void containerize(JibContainerBuilder from, Containerizer to) {
}
}

private File getJibImageTarArchive() {
private File getJibImageTarArchive(Platform platform) {
final BuildDirs buildDirs = new BuildDirs(imageConfiguration.getName(), configuration);
return new File(buildDirs.getTemporaryRootDirectory(), "jib-image." + ArchiveCompression.none.getFileSuffix());
return new File(buildDirs.getTemporaryRootDirectory(), String.format("jib-image.%s-%s.%s",
platform.getOs(), platform.getArchitecture(), ArchiveCompression.none.getFileSuffix()));
}

private Credential getPullRegistryCredentials() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.google.cloud.tools.jib.api.ImageReference;
import com.google.cloud.tools.jib.api.buildplan.Platform;
import org.eclipse.jkube.kit.build.api.assembly.BuildDirs;
import org.eclipse.jkube.kit.common.Assembly;
import org.eclipse.jkube.kit.common.AssemblyFileEntry;
Expand All @@ -49,17 +53,18 @@

public class JibServiceUtil {

private static final String BUSYBOX = "busybox:latest";
private static final Platform DEFAULT_PLATFORM = new Platform("amd64", "linux");

private JibServiceUtil() {
}

private static final String BUSYBOX = "busybox:latest";

public static JibContainerBuilder containerFromImageConfiguration(
ImageConfiguration imageConfiguration, String pullRegistry, Credential pullRegistryCredential
) {
final String baseImage = getBaseImage(imageConfiguration, pullRegistry);
final JibContainerBuilder containerBuilder;
if (baseImage.equals(ImageReference.scratch().toString() + ":latest")) {
if (baseImage.equals(ImageReference.scratch() + ":latest")) {
containerBuilder = Jib.fromScratch();
} else {
containerBuilder = Jib.from(toRegistryImage(baseImage, pullRegistryCredential));
Expand Down Expand Up @@ -113,6 +118,34 @@ static RegistryImage toRegistryImage(String imageReference, Credential credentia
}
}

static ImageReference toImageReference(ImageConfiguration imageConfiguration) {
try {
return ImageReference.parse(imageConfiguration.getName());
} catch (InvalidImageReferenceException e) {
throw new JKubeException("Invalid image reference: " + imageConfiguration.getName(), e);
}
}

static Set<Platform> platforms(ImageConfiguration imageConfiguration) {
final List<String> targetPlatforms = Optional.ofNullable(imageConfiguration)
.map(ImageConfiguration::getBuildConfiguration)
.map(BuildConfiguration::getPlatforms)
.orElse(Collections.emptyList());
final Set<Platform> ret = new LinkedHashSet<>();
for (String targetPlatform : targetPlatforms) {
final int slashIndex = targetPlatform.indexOf('/');
if (slashIndex >= 0) {
final String os = targetPlatform.substring(0, slashIndex);
final String arch = targetPlatform.substring(slashIndex + 1);
ret.add(new Platform(arch, os));
}
}
if (ret.isEmpty()) {
ret.add(DEFAULT_PLATFORM);
}
return ret;
}

public static String getBaseImage(ImageConfiguration imageConfiguration, String optionalRegistry) {
String baseImage = Optional.ofNullable(imageConfiguration)
.map(ImageConfiguration::getBuildConfiguration)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
Expand Down Expand Up @@ -80,9 +81,9 @@ void setUp() {
.pushRegistryConfig(RegistryConfig.builder()
.registry(remoteOciServer)
.settings(Collections.singletonList(RegistryServerConfiguration.builder()
.id(remoteOciServer)
.username("oci-user")
.password("oci-password")
.id(remoteOciServer)
.username("oci-user")
.password("oci-password")
.build()))
.build())
.project(JavaProject.builder()
Expand Down Expand Up @@ -117,13 +118,43 @@ class Build {
@Test
void build() throws Exception {
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
final File jibContainerImageTar = jibService.build();
ArchiveAssertions.assertThat(jibContainerImageTar)
.fileTree()
.contains("manifest.json", "config.json");
final List<File> containerImageTarFiles = jibService.build();
assertThat(containerImageTarFiles)
.singleElement()
.returns("jib-image.linux-amd64.tar", File::getName)
.satisfies(jibContainerImageTar -> {
ArchiveAssertions.assertThat(jibContainerImageTar)
.fileTree()
.contains("manifest.json", "config.json");
});
}
}

@Test
void buildMultiplePlatforms() throws Exception {
imageConfiguration = imageConfiguration.toBuilder()
.build(imageConfiguration.getBuild().toBuilder()
.platform("linux/amd64")
.platform("linux/arm64")
.platform("linux/arm")
.build())
.build();
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
final List<File> containerImageTarFiles = jibService.build();
assertThat(containerImageTarFiles)
.hasSize(3)
.allSatisfy(jibContainerImageTar -> {
ArchiveAssertions.assertThat(jibContainerImageTar)
.fileTree()
.contains("manifest.json", "config.json");
})
.extracting(File::getName)
.contains("jib-image.linux-amd64.tar", "jib-image.linux-arm64.tar", "jib-image.linux-arm.tar");
;
}

}

}

@Nested
Expand All @@ -141,7 +172,7 @@ void setUp() throws Exception {
Jib.fromScratch()
.setFormat(ImageFormat.Docker)
.containerize(Containerizer.to(TarImage
.at(buildDirs.getTemporaryRootDirectory().toPath().resolve("jib-image.tar"))
.at(buildDirs.getTemporaryRootDirectory().toPath().resolve("jib-image.linux-amd64.tar"))
.named(imageConfiguration.getName()))
);
}
Expand All @@ -153,6 +184,7 @@ void tearDown() {
}
}

@SuppressWarnings("resource")
@Test
void emptyImageNameThrowsException() {
final ImageConfiguration emptyImageConfiguration = ImageConfiguration.builder().build();
Expand Down Expand Up @@ -208,6 +240,30 @@ void pushAdditionalTags() throws Exception {
assertThat(IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8))
.contains("{\"name\":\"the-image-name\",\"tags\":[\"1.0\",\"1.0.0\",\"latest\"]}");
}

@Test
void pushMultiplatform() throws Exception {
imageConfiguration = imageConfiguration.toBuilder()
.build(imageConfiguration.getBuild().toBuilder()
.platform("linux/amd64")
.platform("linux/arm64")
.platform("linux/arm")
.build())
.build();
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
jibService.push();
}
final HttpURLConnection connection = (HttpURLConnection) new URL("http://" + remoteOciServer + "/v2/the-image-name/manifests/latest")
.openConnection();
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String("oci-user:oci-password".getBytes()));
connection.setRequestProperty("Accept", "application/vnd.docker.distribution.manifest.list.v2+json");
connection.connect();
assertThat(connection.getResponseCode()).isEqualTo(200);
assertThat(IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8))
.contains(":{\"architecture\":\"amd64\",\"os\":\"linux\"}}")
.contains(":{\"architecture\":\"arm64\",\"os\":\"linux\"}}")
.contains(":{\"architecture\":\"arm\",\"os\":\"linux\"}}");
}
}

}
Loading

0 comments on commit cd64fe7

Please sign in to comment.