Skip to content

Commit cd64fe7

Browse files
authored
feat(jib): multi-platform build support
Signed-off-by: Marc Nuri <[email protected]>
1 parent f4d036a commit cd64fe7

File tree

15 files changed

+549
-162
lines changed

15 files changed

+549
-162
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Usage:
2121
./scripts/extract-changelog-for-version.sh 1.3.37 5
2222
```
2323
### 1.17-SNAPSHOT
24+
* Fix #2098: Add support for multi-platform container image builds in jib build strategy
2425
* Fix #2335: Add support for configuring nodeSelector spec for controller via xml/groovy DSL configuration
2526
* Fix #2459: Allow configuring Buildpacks build via ImageConfiguration
2627
* Fix #2462: `k8s:debug` throws error when using `buildpacks` build strategy

jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/config/property/ConfigKey.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ public enum ConfigKey {
5656
LABELS(ValueCombinePolicy.MERGE),
5757
MAINTAINER,
5858
NAME,
59+
PLATFORMS(ValueCombinePolicy.MERGE),
5960
PORTS(ValueCombinePolicy.MERGE),
6061
REGISTRY,
6162
SHELL,

jkube-kit/build/api/src/main/java/org/eclipse/jkube/kit/build/api/config/property/PropertyConfigResolver.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ private BuildConfiguration extractBuildConfiguration(ImageConfiguration fromConf
8686
.fromExt(valueProvider.getMap(ConfigKey.FROM_EXT, valueOrNull(config, BuildConfiguration::getFromExt)))
8787
.clearVolumes().volumes(valueProvider.getList(ConfigKey.VOLUMES, valueOr(config, BuildConfiguration::getVolumes, Collections.emptyList())))
8888
.clearTags().tags(valueProvider.getList(ConfigKey.TAGS, valueOr(config, BuildConfiguration::getTags, Collections.emptyList())))
89+
.clearPlatforms().platforms(valueProvider.getList(ConfigKey.PLATFORMS, valueOr(config, BuildConfiguration::getPlatforms, Collections.emptyList())))
8990
.maintainer(valueProvider.getString(ConfigKey.MAINTAINER, valueOrNull(config, BuildConfiguration::getMaintainer)))
9091
.workdir(valueProvider.getString(ConfigKey.WORKDIR, valueOrNull(config, BuildConfiguration::getWorkdir)))
9192
.skip(valueProvider.getBoolean(ConfigKey.SKIP, valueOrNull(config, BuildConfiguration::getSkip)))

jkube-kit/build/api/src/test/java/org/eclipse/jkube/kit/build/api/config/property/PropertyConfigResolverTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ void setUp() {
5151
.port("9082")
5252
.tag("initial-tag-1")
5353
.tag("initial-tag-2")
54+
.platform("darwin/amd64")
5455
.healthCheck(HealthCheckConfiguration.builder()
5556
.interval("30s")
5657
.build())
@@ -73,6 +74,8 @@ void setUp() {
7374
javaProject.getProperties().put("jkube.container-image.ports.2", "9080");
7475
javaProject.getProperties().put("jkube.container-image.tags.1", "tag-1");
7576
javaProject.getProperties().put("jkube.container-image.tags.2", "tag-2");
77+
javaProject.getProperties().put("jkube.container-image.platforms.1", "linux/amd64");
78+
javaProject.getProperties().put("jkube.container-image.platforms.2", "linux/arm64");
7679
javaProject.getProperties().put("jkube.container-image.healthcheck.interval", "10s");
7780
}
7881

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

155+
152156
@Test
153157
void setsTags() {
154158
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("tag-1", "tag-2");
155159
}
156160

161+
@Test
162+
void setsPlatforms() {
163+
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("linux/amd64", "linux/arm64");
164+
}
165+
157166
@Test
158167
void setsHealthCheckInterval() {
159168
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("10s");
@@ -225,6 +234,11 @@ void appendsTags() {
225234
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("tag-1", "tag-2", "initial-tag-1", "initial-tag-2");
226235
}
227236

237+
@Test
238+
void appendsPlatforms() {
239+
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("linux/amd64", "linux/arm64", "darwin/amd64");
240+
}
241+
228242
@Test
229243
void overridesHealthCheckInterval() {
230244
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("10s");
@@ -292,6 +306,11 @@ void preservesTags() {
292306
assertThat(resolved.getBuild().getTags()).containsExactlyInAnyOrder("initial-tag-1", "initial-tag-2");
293307
}
294308

309+
@Test
310+
void preservesPlatforms() {
311+
assertThat(resolved.getBuild().getPlatforms()).containsExactlyInAnyOrder("darwin/amd64");
312+
}
313+
295314
@Test
296315
void preservesHealthCheckInterval() {
297316
assertThat(resolved.getBuild().getHealthCheck().getInterval()).isEqualTo("30s");

jkube-kit/build/service/jib/pom.xml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,14 @@
4747
<groupId>org.apache.commons</groupId>
4848
<artifactId>commons-text</artifactId>
4949
</dependency>
50+
5051
<dependency>
51-
<groupId>org.mockito</groupId>
52-
<artifactId>mockito-core</artifactId>
52+
<groupId>org.junit.jupiter</groupId>
53+
<artifactId>junit-jupiter-engine</artifactId>
5354
</dependency>
5455
<dependency>
5556
<groupId>org.junit.jupiter</groupId>
56-
<artifactId>junit-jupiter-engine</artifactId>
57-
<scope>test</scope>
57+
<artifactId>junit-jupiter-params</artifactId>
5858
</dependency>
5959
<dependency>
6060
<groupId>org.assertj</groupId>

jkube-kit/build/service/jib/src/main/java/org/eclipse/jkube/kit/service/jib/JibService.java

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,12 @@
1616
import com.google.cloud.tools.jib.api.CacheDirectoryCreationException;
1717
import com.google.cloud.tools.jib.api.Containerizer;
1818
import com.google.cloud.tools.jib.api.Credential;
19-
import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
2019
import com.google.cloud.tools.jib.api.Jib;
2120
import com.google.cloud.tools.jib.api.JibContainerBuilder;
2221
import com.google.cloud.tools.jib.api.LogEvent;
2322
import com.google.cloud.tools.jib.api.RegistryException;
2423
import com.google.cloud.tools.jib.api.TarImage;
24+
import com.google.cloud.tools.jib.api.buildplan.Platform;
2525
import com.google.cloud.tools.jib.event.events.ProgressEvent;
2626
import org.eclipse.jkube.kit.build.api.assembly.AssemblyManager;
2727
import org.eclipse.jkube.kit.build.api.assembly.BuildDirs;
@@ -39,8 +39,11 @@
3939
import java.io.File;
4040
import java.io.IOException;
4141
import java.time.Instant;
42+
import java.util.ArrayList;
43+
import java.util.Collections;
4244
import java.util.List;
4345
import java.util.Map;
46+
import java.util.Set;
4447
import java.util.concurrent.ExecutionException;
4548
import java.util.concurrent.ExecutorService;
4649
import java.util.concurrent.Executors;
@@ -49,6 +52,8 @@
4952
import static org.eclipse.jkube.kit.build.api.helper.RegistryUtil.getApplicablePullRegistryFrom;
5053
import static org.eclipse.jkube.kit.build.api.helper.RegistryUtil.getApplicablePushRegistryFrom;
5154
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.containerFromImageConfiguration;
55+
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.platforms;
56+
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.toImageReference;
5257
import static org.eclipse.jkube.kit.service.jib.JibServiceUtil.toRegistryImage;
5358

5459
public class JibService implements AutoCloseable {
@@ -89,23 +94,33 @@ public final ImageName getImageName() {
8994
/**
9095
* Builds a container Jib container image tarball.
9196
*
92-
* @return the location of the generated tarball file.
97+
* @return the location of the generated tarball files.
9398
*/
94-
public final File build() {
95-
final JibContainerBuilder from = assembleFrom();
96-
try {
97-
final File jibImageTarArchive = getJibImageTarArchive();
98-
final Containerizer to = Containerizer
99-
.to(TarImage.at(jibImageTarArchive.toPath()).named(imageConfiguration.getName()));
99+
public final List<File> build() {
100+
final List<File> generatedTarballs = new ArrayList<>();
101+
for (Platform platform : platforms(imageConfiguration)) {
102+
final JibContainerBuilder from = assembleFrom();
103+
from.setPlatforms(Collections.singleton(platform));
104+
final File jibImageTarArchive = getJibImageTarArchive(platform);
105+
final Containerizer to = Containerizer.to(
106+
TarImage.at(jibImageTarArchive.toPath())
107+
.named(toImageReference(imageConfiguration))
108+
);
100109
containerize(from, to);
101-
return jibImageTarArchive;
102-
} catch (InvalidImageReferenceException ex) {
103-
throw new JKubeException("Unable to build the image tarball: " + ex.getMessage(), ex);
110+
generatedTarballs.add(jibImageTarArchive);
104111
}
112+
return generatedTarballs;
105113
}
106114

107115
public final void push() {
108-
final JibContainerBuilder from = Jib.from(TarImage.at(getJibImageTarArchive().toPath()));
116+
final Set<Platform> platforms = platforms(imageConfiguration);
117+
final JibContainerBuilder from;
118+
if (platforms.size() > 1) {
119+
from = assembleFrom();
120+
from.setPlatforms(platforms);
121+
} else {
122+
from = Jib.from(TarImage.at(getJibImageTarArchive(platforms.iterator().next()).toPath()));
123+
}
109124
final Containerizer to = Containerizer
110125
.to(toRegistryImage(getImageName().getFullName(), getPushRegistryCredentials()));
111126
containerize(from, to);
@@ -158,9 +173,10 @@ private void containerize(JibContainerBuilder from, Containerizer to) {
158173
}
159174
}
160175

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

166182
private Credential getPullRegistryCredentials() {

jkube-kit/build/service/jib/src/main/java/org/eclipse/jkube/kit/service/jib/JibServiceUtil.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,17 @@
1616
import java.io.File;
1717
import java.nio.file.Path;
1818
import java.util.ArrayList;
19+
import java.util.Collections;
20+
import java.util.LinkedHashSet;
1921
import java.util.List;
2022
import java.util.Map;
2123
import java.util.Optional;
24+
import java.util.Set;
2225
import java.util.function.Predicate;
2326
import java.util.stream.Collectors;
2427

2528
import com.google.cloud.tools.jib.api.ImageReference;
29+
import com.google.cloud.tools.jib.api.buildplan.Platform;
2630
import org.eclipse.jkube.kit.build.api.assembly.BuildDirs;
2731
import org.eclipse.jkube.kit.common.Assembly;
2832
import org.eclipse.jkube.kit.common.AssemblyFileEntry;
@@ -49,17 +53,18 @@
4953

5054
public class JibServiceUtil {
5155

56+
private static final String BUSYBOX = "busybox:latest";
57+
private static final Platform DEFAULT_PLATFORM = new Platform("amd64", "linux");
58+
5259
private JibServiceUtil() {
5360
}
5461

55-
private static final String BUSYBOX = "busybox:latest";
56-
5762
public static JibContainerBuilder containerFromImageConfiguration(
5863
ImageConfiguration imageConfiguration, String pullRegistry, Credential pullRegistryCredential
5964
) {
6065
final String baseImage = getBaseImage(imageConfiguration, pullRegistry);
6166
final JibContainerBuilder containerBuilder;
62-
if (baseImage.equals(ImageReference.scratch().toString() + ":latest")) {
67+
if (baseImage.equals(ImageReference.scratch() + ":latest")) {
6368
containerBuilder = Jib.fromScratch();
6469
} else {
6570
containerBuilder = Jib.from(toRegistryImage(baseImage, pullRegistryCredential));
@@ -113,6 +118,34 @@ static RegistryImage toRegistryImage(String imageReference, Credential credentia
113118
}
114119
}
115120

121+
static ImageReference toImageReference(ImageConfiguration imageConfiguration) {
122+
try {
123+
return ImageReference.parse(imageConfiguration.getName());
124+
} catch (InvalidImageReferenceException e) {
125+
throw new JKubeException("Invalid image reference: " + imageConfiguration.getName(), e);
126+
}
127+
}
128+
129+
static Set<Platform> platforms(ImageConfiguration imageConfiguration) {
130+
final List<String> targetPlatforms = Optional.ofNullable(imageConfiguration)
131+
.map(ImageConfiguration::getBuildConfiguration)
132+
.map(BuildConfiguration::getPlatforms)
133+
.orElse(Collections.emptyList());
134+
final Set<Platform> ret = new LinkedHashSet<>();
135+
for (String targetPlatform : targetPlatforms) {
136+
final int slashIndex = targetPlatform.indexOf('/');
137+
if (slashIndex >= 0) {
138+
final String os = targetPlatform.substring(0, slashIndex);
139+
final String arch = targetPlatform.substring(slashIndex + 1);
140+
ret.add(new Platform(arch, os));
141+
}
142+
}
143+
if (ret.isEmpty()) {
144+
ret.add(DEFAULT_PLATFORM);
145+
}
146+
return ret;
147+
}
148+
116149
public static String getBaseImage(ImageConfiguration imageConfiguration, String optionalRegistry) {
117150
String baseImage = Optional.ofNullable(imageConfiguration)
118151
.map(ImageConfiguration::getBuildConfiguration)

jkube-kit/build/service/jib/src/test/java/org/eclipse/jkube/kit/service/jib/JibServiceTest.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.nio.charset.StandardCharsets;
4949
import java.nio.file.Path;
5050
import java.util.Collections;
51+
import java.util.List;
5152

5253
import static org.assertj.core.api.Assertions.assertThat;
5354
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -80,9 +81,9 @@ void setUp() {
8081
.pushRegistryConfig(RegistryConfig.builder()
8182
.registry(remoteOciServer)
8283
.settings(Collections.singletonList(RegistryServerConfiguration.builder()
83-
.id(remoteOciServer)
84-
.username("oci-user")
85-
.password("oci-password")
84+
.id(remoteOciServer)
85+
.username("oci-user")
86+
.password("oci-password")
8687
.build()))
8788
.build())
8889
.project(JavaProject.builder()
@@ -117,13 +118,43 @@ class Build {
117118
@Test
118119
void build() throws Exception {
119120
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
120-
final File jibContainerImageTar = jibService.build();
121-
ArchiveAssertions.assertThat(jibContainerImageTar)
122-
.fileTree()
123-
.contains("manifest.json", "config.json");
121+
final List<File> containerImageTarFiles = jibService.build();
122+
assertThat(containerImageTarFiles)
123+
.singleElement()
124+
.returns("jib-image.linux-amd64.tar", File::getName)
125+
.satisfies(jibContainerImageTar -> {
126+
ArchiveAssertions.assertThat(jibContainerImageTar)
127+
.fileTree()
128+
.contains("manifest.json", "config.json");
129+
});
124130
}
125131
}
126132

133+
@Test
134+
void buildMultiplePlatforms() throws Exception {
135+
imageConfiguration = imageConfiguration.toBuilder()
136+
.build(imageConfiguration.getBuild().toBuilder()
137+
.platform("linux/amd64")
138+
.platform("linux/arm64")
139+
.platform("linux/arm")
140+
.build())
141+
.build();
142+
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
143+
final List<File> containerImageTarFiles = jibService.build();
144+
assertThat(containerImageTarFiles)
145+
.hasSize(3)
146+
.allSatisfy(jibContainerImageTar -> {
147+
ArchiveAssertions.assertThat(jibContainerImageTar)
148+
.fileTree()
149+
.contains("manifest.json", "config.json");
150+
})
151+
.extracting(File::getName)
152+
.contains("jib-image.linux-amd64.tar", "jib-image.linux-arm64.tar", "jib-image.linux-arm.tar");
153+
;
154+
}
155+
156+
}
157+
127158
}
128159

129160
@Nested
@@ -141,7 +172,7 @@ void setUp() throws Exception {
141172
Jib.fromScratch()
142173
.setFormat(ImageFormat.Docker)
143174
.containerize(Containerizer.to(TarImage
144-
.at(buildDirs.getTemporaryRootDirectory().toPath().resolve("jib-image.tar"))
175+
.at(buildDirs.getTemporaryRootDirectory().toPath().resolve("jib-image.linux-amd64.tar"))
145176
.named(imageConfiguration.getName()))
146177
);
147178
}
@@ -153,6 +184,7 @@ void tearDown() {
153184
}
154185
}
155186

187+
@SuppressWarnings("resource")
156188
@Test
157189
void emptyImageNameThrowsException() {
158190
final ImageConfiguration emptyImageConfiguration = ImageConfiguration.builder().build();
@@ -208,6 +240,30 @@ void pushAdditionalTags() throws Exception {
208240
assertThat(IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8))
209241
.contains("{\"name\":\"the-image-name\",\"tags\":[\"1.0\",\"1.0.0\",\"latest\"]}");
210242
}
243+
244+
@Test
245+
void pushMultiplatform() throws Exception {
246+
imageConfiguration = imageConfiguration.toBuilder()
247+
.build(imageConfiguration.getBuild().toBuilder()
248+
.platform("linux/amd64")
249+
.platform("linux/arm64")
250+
.platform("linux/arm")
251+
.build())
252+
.build();
253+
try (JibService jibService = new JibService(jibLogger, testAuthConfigFactory, configuration, imageConfiguration)) {
254+
jibService.push();
255+
}
256+
final HttpURLConnection connection = (HttpURLConnection) new URL("http://" + remoteOciServer + "/v2/the-image-name/manifests/latest")
257+
.openConnection();
258+
connection.setRequestProperty("Authorization", "Basic " + Base64.encodeBase64String("oci-user:oci-password".getBytes()));
259+
connection.setRequestProperty("Accept", "application/vnd.docker.distribution.manifest.list.v2+json");
260+
connection.connect();
261+
assertThat(connection.getResponseCode()).isEqualTo(200);
262+
assertThat(IOUtils.toString(connection.getInputStream(), StandardCharsets.UTF_8))
263+
.contains(":{\"architecture\":\"amd64\",\"os\":\"linux\"}}")
264+
.contains(":{\"architecture\":\"arm64\",\"os\":\"linux\"}}")
265+
.contains(":{\"architecture\":\"arm\",\"os\":\"linux\"}}");
266+
}
211267
}
212268

213269
}

0 commit comments

Comments
 (0)