Skip to content

Commit 8a24c84

Browse files
authored
Implement canMount (#650)
Signed-off-by: Valentin Delaye <jonesbusy@users.noreply.github.com>
1 parent 1dfd1bd commit 8a24c84

7 files changed

Lines changed: 109 additions & 0 deletions

File tree

src/main/java/land/oras/ContainerRef.java

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,32 @@ public String getTagsPath() {
313313
return getTagsPath(null, null);
314314
}
315315

316+
/**
317+
* Return the blobs mount URL for cross-repository blob mounting
318+
* @param sourceRef The source container reference to mount the blob from
319+
* @return The blobs mount URL
320+
*/
321+
public String getBlobsMountPath(ContainerRef sourceRef) {
322+
return getBlobsMountPath(null, sourceRef);
323+
}
324+
325+
/**
326+
* Return the blobs mount URL for cross-repository blob mounting
327+
* @param registry The registry
328+
* @param sourceRef The source container reference to mount the blob from
329+
* @return The blobs mount URL
330+
*/
331+
public String getBlobsMountPath(@Nullable Registry registry, ContainerRef sourceRef) {
332+
if (digest == null) {
333+
throw new OrasException("You are required to include a digest");
334+
}
335+
return "%s/blobs/uploads/?mount=%s&from=%s"
336+
.formatted(
337+
getApiPrefix(registry),
338+
digest,
339+
URLEncoder.encode(sourceRef.getFullRepository(registry), StandardCharsets.UTF_8));
340+
}
341+
316342
/**
317343
* Return the referrers URL for this container referrer
318344
* @param artifactType The optional artifact type

src/main/java/land/oras/OCI.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,15 @@ public Layer pushBlob(T ref, Path blob) {
106106
return pushBlob(ref, blob, Map.of());
107107
}
108108

109+
/**
110+
* Return whether this OCI instance supports mounting blobs from the given source OCI instance.
111+
* @param other The source OCI instance to check compatibility with
112+
* @param sourceRef The source reference
113+
* @param targetRef The target reference
114+
* @return {@code true} if mounting from {@code other} is supported
115+
*/
116+
public abstract boolean canMount(OCI<?> other, T sourceRef, T targetRef);
117+
109118
/**
110119
* Push a blob stream. Creates a temporary file to store the blob and push the file. The temporary file will be deleted after pushing
111120
* @param ref The ref

src/main/java/land/oras/OCILayout.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ public final class OCILayout extends OCI<LayoutRef> {
5959
*/
6060
private OCILayout() {}
6161

62+
@Override
63+
public boolean canMount(OCI<?> other, LayoutRef sourceRef, LayoutRef targetRef) {
64+
if (!(other instanceof OCILayout)) {
65+
return false;
66+
}
67+
// They reference the same layout
68+
return sourceRef.getFolder().equals(targetRef.getFolder());
69+
}
70+
6271
/**
6372
* Return a new builder for this oci layout
6473
* @return The builder

src/main/java/land/oras/Registry.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import java.nio.file.Files;
3131
import java.nio.file.Path;
3232
import java.nio.file.StandardCopyOption;
33+
import java.security.MessageDigest;
3334
import java.util.HashMap;
3435
import java.util.List;
3536
import java.util.Map;
@@ -114,6 +115,38 @@ private Registry() {
114115
this.registriesConf = RegistriesConf.newConf();
115116
}
116117

118+
@Override
119+
public boolean canMount(OCI<?> other, ContainerRef sourceRef, ContainerRef targetRef) {
120+
if (!(other instanceof Registry otherRegistry)) {
121+
LOG.debug("Other OCI is not a registry, cannot mount");
122+
return false;
123+
}
124+
// Not the same registry
125+
String effectiveSourceRegistry = sourceRef.getEffectiveRegistry(this);
126+
String effectiveTargetRegistry = targetRef.getEffectiveRegistry(otherRegistry);
127+
if (!effectiveSourceRegistry.equals(effectiveTargetRegistry)) {
128+
LOG.debug(
129+
"Cannot mount blob from registry {} to registry {}",
130+
effectiveSourceRegistry,
131+
effectiveTargetRegistry);
132+
return false;
133+
}
134+
// Not the same auth
135+
String authHeaderSource = authProvider.getAuthHeader(sourceRef);
136+
String authHeaderTarget = otherRegistry.authProvider.getAuthHeader(targetRef);
137+
if (!(authHeaderSource == authHeaderTarget
138+
|| (authHeaderSource != null
139+
&& authHeaderTarget != null
140+
&& MessageDigest.isEqual(
141+
authHeaderSource.getBytes(StandardCharsets.UTF_8),
142+
authHeaderTarget.getBytes(StandardCharsets.UTF_8))))) {
143+
LOG.debug("Authentication is different between source and target registry, cannot mount");
144+
return false;
145+
}
146+
LOG.debug("Blob can be mounted from registry {} to registry {}", sourceRef, targetRef);
147+
return true;
148+
}
149+
117150
/**
118151
* Get the registries configuration
119152
* @return The registries configuration

src/test/java/land/oras/ContainerRefTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ void shouldParseImageWithNoNamespace() {
263263
assertEquals("alpine", containerRef.getRepository());
264264
assertEquals("latest", containerRef.getTag());
265265
assertEquals("sha256:1234567890abcdef", containerRef.getDigest());
266+
assertEquals(
267+
"demo.goharbor.com/v2/alpine/blobs/uploads/?mount=sha256:1234567890abcdef&from=alpine",
268+
containerRef.getBlobsMountPath(containerRef));
266269
}
267270

268271
@Test

src/test/java/land/oras/OCILayoutTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,10 @@ void shouldPushToOciLayoutWithoutTag() throws IOException {
425425
assertTrue(
426426
index.getManifests().get(1).getAnnotations().containsKey(Const.ANNOTATION_CREATED),
427427
"Should have created annotation");
428+
429+
// Check if we can mount blobs
430+
assertTrue(ociLayout.canMount(ociLayout, layoutRef, layoutRef));
431+
assertFalse(ociLayout.canMount(ociLayout, layoutRef, layoutRef.forTarget("other")));
428432
}
429433

430434
@Test

src/test/java/land/oras/RegistryTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,31 @@ void shouldPushBlobWithDigestViaStream() {
250250
registry.pushBlob(containerRef, size, () -> stream, Map.of());
251251
}
252252

253+
@Test
254+
void shouldCheckIfCanMount() {
255+
Registry registry = Registry.builder()
256+
.insecure(this.registry.getRegistry(), "myuser", "mypass")
257+
.build();
258+
Registry otherRegistry = Registry.builder()
259+
.insecure("localhost:8080", "myuser", "myuser")
260+
.build();
261+
Registry otherAuth = Registry.builder()
262+
.insecure(this.registry.getRegistry(), "bar", "foo")
263+
.build();
264+
byte[] content = "foo".getBytes(StandardCharsets.UTF_8);
265+
String digest = SupportedAlgorithm.getDefault().digest(content);
266+
long size = content.length;
267+
InputStream stream = new ByteArrayInputStream(content);
268+
ContainerRef containerRef = ContainerRef.parse("library/artifact-mount").withDigest(digest);
269+
registry.pushBlob(containerRef, size, () -> stream, Map.of());
270+
assertTrue(registry.canMount(registry, containerRef, containerRef), "Should mount same reference");
271+
assertFalse(
272+
registry.canMount(otherRegistry, containerRef, containerRef), "Should not mount if different registry");
273+
assertFalse(
274+
registry.canMount(otherAuth, containerRef, containerRef),
275+
"Should not mount if different authentication");
276+
}
277+
253278
@Test
254279
void shouldPushAndGetBlobThenDeleteWithSha256() {
255280
Registry registry = Registry.Builder.builder()

0 commit comments

Comments
 (0)