Skip to content

Commit bf575e6

Browse files
registry: implement pull and extract in AbstractRegistry (#314)
Co-authored-by: Abhilaksh Sharma <iamabhilakshsharma@gmail.com>
1 parent 30be422 commit bf575e6

File tree

9 files changed

+936
-189
lines changed

9 files changed

+936
-189
lines changed

registry/registry-client/src/main/java/com/salesforce/multicloudj/registry/driver/AbstractRegistry.java

Lines changed: 129 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,28 @@
22

33
import com.salesforce.multicloudj.common.exceptions.InvalidArgumentException;
44
import com.salesforce.multicloudj.common.exceptions.SubstrateSdkException;
5-
import com.salesforce.multicloudj.common.exceptions.UnSupportedOperationException;
5+
import com.salesforce.multicloudj.common.exceptions.UnknownException;
66
import com.salesforce.multicloudj.common.provider.Provider;
77
import com.salesforce.multicloudj.registry.model.Image;
8+
import com.salesforce.multicloudj.registry.model.Layer;
9+
import com.salesforce.multicloudj.registry.model.Manifest;
810
import com.salesforce.multicloudj.registry.model.Platform;
911
import com.salesforce.multicloudj.sts.model.CredentialsOverrider;
1012
import lombok.Getter;
1113
import org.apache.commons.lang3.StringUtils;
1214

1315
import java.io.InputStream;
1416
import java.net.URI;
17+
import java.util.List;
18+
import java.util.stream.Collectors;
1519

1620
/**
1721
* Abstract registry driver. Each cloud implements authentication and OCI client.
1822
*/
1923
public abstract class AbstractRegistry implements Provider, AutoCloseable, AuthProvider {
24+
25+
private static final String UNKNOWN = "unknown";
26+
2027
protected final String providerId;
2128
protected final String registryEndpoint;
2229
protected final URI proxyEndpoint;
@@ -32,7 +39,7 @@ protected AbstractRegistry(Builder<?, ?> builder) {
3239
this.targetPlatform = builder.getPlatform() != null ? builder.getPlatform() : Platform.DEFAULT;
3340

3441
if (StringUtils.isBlank(registryEndpoint)) {
35-
throw new InvalidArgumentException("Registry endpoint is not configured.");
42+
throw new InvalidArgumentException("Registry endpoint is required");
3643
}
3744
}
3845

@@ -57,26 +64,138 @@ public String getProviderId() {
5764

5865
/**
5966
* Pulls an image from the registry (unified OCI flow).
67+
*
68+
* <p>This method implements the OCI image pull workflow:
69+
* <ol>
70+
* <li>Parse the image reference to extract repository and reference (tag or digest)</li>
71+
* <li>Fetch the manifest from the registry</li>
72+
* <li>If the manifest is a multi-arch index, select a platform-specific manifest</li>
73+
* <li>Return a RemoteImage that lazily loads layers on demand</li>
74+
* </ol>
75+
*
76+
* <p>The returned Image uses lazy loading - blobs are only downloaded when accessed
77+
* via {@link Image#getLayers()}.
6078
*
61-
* @param imageRef image reference (e.g. repo:tag or digest)
62-
* @return Image metadata and layer descriptors
79+
* @param imageRef image reference (e.g. "my-repo/my-image:latest" or "my-repo/my-image@sha256:...")
80+
* @return Image metadata and layer descriptors (lazy-loaded)
81+
* @throws InvalidArgumentException if the image reference format is invalid
82+
* @throws UnknownException if the pull fails (network error, authentication failure, manifest not found, etc.)
6383
*/
6484
public Image pull(String imageRef) {
65-
// TODO: need to be implemented
66-
throw new UnSupportedOperationException("pull() not yet implemented");
85+
if (StringUtils.isBlank(imageRef)) {
86+
throw new InvalidArgumentException("Image reference cannot be null or empty");
87+
}
88+
89+
// Step 1: Parse image reference
90+
ImageReference imageReference = ImageReference.parse(imageRef);
91+
String repository = imageReference.getRepository();
92+
String reference = imageReference.getReference();
93+
94+
// Step 2: Get OCI client
95+
OciRegistryClient client = getOciClient();
96+
97+
// Step 3: Fetch manifest
98+
Manifest manifest = client.fetchManifest(repository, reference);
99+
100+
// Step 4: Handle multi-arch image index
101+
if (manifest.isIndex()) {
102+
String selectedDigest = selectPlatformFromIndex(manifest, targetPlatform);
103+
manifest = client.fetchManifest(repository, selectedDigest);
104+
}
105+
106+
// Step 5: Create and return RemoteImage (lazy-loading)
107+
return new RemoteImage(client, repository, imageRef, manifest);
67108
}
68109

69110
/**
70-
* Extracts the image filesystem as a tar stream (OCI layer flattening, reverse order, whiteout handling).
111+
* Selects a platform-specific manifest from a multi-arch image index.
112+
*
113+
* <p>This method matches the target platform against each entry in the index
114+
* using the Platform.matches() method. The first matching entry is selected.
115+
*
116+
* <p>Platform matching follows OCI specification:
117+
* <ul>
118+
* <li>OS, Architecture, Variant, and OS version must match</li>
119+
* <li>OS features in the spec must be a subset of the entry's OS features</li>
120+
* <li>Empty/null fields in the target platform are treated as wildcards</li>
121+
* </ul>
122+
*
123+
* @param indexManifest the image index manifest (must be an index)
124+
* @param platform the target platform to match against
125+
* @return the digest of the selected platform-specific manifest
126+
* @throws UnknownException if no matching platform is found in the index
127+
* @throws InvalidArgumentException if the manifest is not an index
128+
*/
129+
protected String selectPlatformFromIndex(Manifest indexManifest, Platform platform) {
130+
if (!indexManifest.isIndex()) {
131+
throw new InvalidArgumentException("Manifest is not an index");
132+
}
133+
134+
List<Manifest.IndexEntry> entries = indexManifest.getIndexManifests();
135+
if (entries == null || entries.isEmpty()) {
136+
throw new UnknownException("Image index contains no platform entries");
137+
}
138+
139+
// Find first matching platform entry
140+
for (Manifest.IndexEntry entry : entries) {
141+
Platform entryPlatform = entry.getPlatform();
142+
if (entryPlatform != null && entryPlatform.matches(platform)) {
143+
return entry.getDigest();
144+
}
145+
}
146+
147+
String targetOperatingSystem = platform.getOperatingSystem();
148+
String targetArchitecture = platform.getArchitecture();
149+
throw new UnknownException(String.format(
150+
"No manifest found for platform %s/%s in image index. Available platforms: %s",
151+
targetOperatingSystem != null ? targetOperatingSystem : UNKNOWN,
152+
targetArchitecture != null ? targetArchitecture : UNKNOWN,
153+
formatAvailablePlatforms(entries)));
154+
}
155+
156+
/**
157+
* Formats available platforms from index entries for error messages.
158+
*/
159+
private String formatAvailablePlatforms(List<Manifest.IndexEntry> entries) {
160+
return entries.stream()
161+
.map(entry -> String.format("%s/%s",
162+
entry.getOs() != null ? entry.getOs() : UNKNOWN,
163+
entry.getArchitecture() != null ? entry.getArchitecture() : UNKNOWN))
164+
.collect(Collectors.joining(", "));
165+
}
166+
167+
/**
168+
* Extracts the image filesystem as a tar stream.
169+
*
170+
* <p>This method implements OCI layer flattening.
171+
* <ol>
172+
* <li>Layers are processed in order (bottom to top)</li>
173+
* <li>Each layer is a tar archive that is decompressed and streamed</li>
174+
* <li>Whiteout files (.wh.*) mark deletions from lower layers</li>
175+
* <li>Opaque whiteouts (.wh..wh..opq) indicate directory replacement</li>
176+
* </ol>
177+
*
178+
* <p>The returned InputStream produces a tar archive representing the flattened
179+
* filesystem. Caller is responsible for closing the stream.
71180
*
72181
* @param image image from a previous pull
73182
* @return InputStream of the flattened filesystem tar
183+
* @throws InvalidArgumentException if image is null or has no layers
184+
* @throws UnknownException if extraction fails
74185
*/
75186
public InputStream extract(Image image) {
76-
// TODO: implement OCI layer flattening (reverse order, whiteout handling)
77-
throw new UnSupportedOperationException("extract() not yet implemented");
78-
}
187+
if (image == null) {
188+
throw new InvalidArgumentException("Image cannot be null");
189+
}
79190

191+
List<Layer> layers = image.getLayers();
192+
if (layers == null || layers.isEmpty()) {
193+
throw new InvalidArgumentException("Image has no layers to extract");
194+
}
195+
196+
// Create a LayerExtractor that handles the flattening logic
197+
return new LayerExtractor(layers).extract();
198+
}
80199

81200
public abstract Class<? extends SubstrateSdkException> getException(Throwable t);
82201

0 commit comments

Comments
 (0)