22
33import com .salesforce .multicloudj .common .exceptions .InvalidArgumentException ;
44import com .salesforce .multicloudj .common .exceptions .SubstrateSdkException ;
5- import com .salesforce .multicloudj .common .exceptions .UnSupportedOperationException ;
5+ import com .salesforce .multicloudj .common .exceptions .UnknownException ;
66import com .salesforce .multicloudj .common .provider .Provider ;
77import com .salesforce .multicloudj .registry .model .Image ;
8+ import com .salesforce .multicloudj .registry .model .Layer ;
9+ import com .salesforce .multicloudj .registry .model .Manifest ;
810import com .salesforce .multicloudj .registry .model .Platform ;
911import com .salesforce .multicloudj .sts .model .CredentialsOverrider ;
1012import lombok .Getter ;
1113import org .apache .commons .lang3 .StringUtils ;
1214
1315import java .io .InputStream ;
1416import 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 */
1923public 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