Skip to content

Commit 5518c22

Browse files
committed
ContainerRegistry: publishContainerImage should not be a method of RegistryClient
1 parent 935912e commit 5518c22

File tree

2 files changed

+185
-167
lines changed

2 files changed

+185
-167
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftContainerPlugin open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the SwiftContainerPlugin project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftContainerPlugin project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import struct Foundation.Date
16+
import struct Foundation.URL
17+
18+
import ContainerRegistry
19+
import Tar
20+
21+
func publishContainerImage(
22+
baseImage: ImageReference,
23+
destinationImage: ImageReference,
24+
source: RegistryClient?,
25+
destination: RegistryClient,
26+
architecture: String,
27+
os: String,
28+
resources: [String],
29+
tag: String?,
30+
verbose: Bool,
31+
executableURL: URL
32+
) async throws -> ImageReference {
33+
34+
// MARK: Find the base image
35+
36+
let baseImageManifest: ImageManifest
37+
let baseImageConfiguration: ImageConfiguration
38+
let baseImageDescriptor: ContentDescriptor
39+
if let source {
40+
(baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
41+
forImage: baseImage,
42+
architecture: architecture
43+
)
44+
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
45+
46+
baseImageConfiguration = try await source.getImageConfiguration(
47+
forImage: baseImage,
48+
digest: ImageReference.Digest(baseImageManifest.config.digest)
49+
)
50+
log("Found base image configuration: \(baseImageManifest.config.digest)")
51+
} else {
52+
baseImageManifest = .init(
53+
schemaVersion: 2,
54+
config: .init(mediaType: "scratch", digest: "scratch", size: 0),
55+
layers: []
56+
)
57+
baseImageConfiguration = .init(
58+
architecture: architecture,
59+
os: os,
60+
rootfs: .init(_type: "layers", diff_ids: [])
61+
)
62+
if verbose { log("Using scratch as base image") }
63+
}
64+
65+
// MARK: Upload resource layers
66+
67+
var resourceLayers: [RegistryClient.ImageLayer] = []
68+
for resourceDir in resources {
69+
let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes
70+
let resourceLayer = try await destination.uploadLayer(
71+
repository: destinationImage.repository,
72+
contents: resourceTardiff
73+
)
74+
75+
if verbose {
76+
log("resource layer: \(resourceLayer.descriptor.digest) (\(resourceLayer.descriptor.size) bytes)")
77+
}
78+
79+
resourceLayers.append(resourceLayer)
80+
}
81+
82+
// MARK: Upload the application layer
83+
84+
let applicationLayer = try await destination.uploadLayer(
85+
repository: destinationImage.repository,
86+
contents: try Archive().appendingFile(at: executableURL).bytes
87+
)
88+
if verbose {
89+
log("application layer: \(applicationLayer.descriptor.digest) (\(applicationLayer.descriptor.size) bytes)")
90+
}
91+
92+
// MARK: Create the application configuration
93+
94+
let timestamp = Date(timeIntervalSince1970: 0).ISO8601Format()
95+
96+
// Inherit the configuration of the base image - UID, GID, environment etc -
97+
// and override the entrypoint.
98+
var inheritedConfiguration = baseImageConfiguration.config ?? .init()
99+
inheritedConfiguration.Entrypoint = ["/\(executableURL.lastPathComponent)"]
100+
inheritedConfiguration.Cmd = []
101+
inheritedConfiguration.WorkingDir = "/"
102+
103+
let configuration = ImageConfiguration(
104+
created: timestamp,
105+
architecture: architecture,
106+
os: os,
107+
config: inheritedConfiguration,
108+
rootfs: .init(
109+
_type: "layers",
110+
// The diff_id is the digest of the _uncompressed_ layer archive.
111+
// It is used by the runtime, which might not store the layers in
112+
// the compressed form in which it received them from the registry.
113+
diff_ids: baseImageConfiguration.rootfs.diff_ids
114+
+ resourceLayers.map { "\($0.diffID)" }
115+
+ ["\(applicationLayer.diffID)"]
116+
),
117+
history: [.init(created: timestamp, created_by: "containertool")]
118+
)
119+
120+
let configurationBlobReference = try await destination.putImageConfiguration(
121+
forImage: destinationImage,
122+
configuration: configuration
123+
)
124+
125+
if verbose {
126+
log("image configuration: \(configurationBlobReference.digest) (\(configurationBlobReference.size) bytes)")
127+
}
128+
129+
// MARK: Create application manifest
130+
131+
let manifest = ImageManifest(
132+
schemaVersion: 2,
133+
mediaType: "application/vnd.oci.image.manifest.v1+json",
134+
config: configurationBlobReference,
135+
layers: baseImageManifest.layers
136+
+ resourceLayers.map { $0.descriptor }
137+
+ [applicationLayer.descriptor]
138+
)
139+
140+
// MARK: Upload base image
141+
142+
// Copy the base image layers to the destination repository
143+
// Layers could be checked and uploaded concurrently
144+
// This could also happen in parallel with the application image build
145+
if let source {
146+
for layer in baseImageManifest.layers {
147+
try await source.copyBlob(
148+
digest: ImageReference.Digest(layer.digest),
149+
fromRepository: baseImage.repository,
150+
toClient: destination,
151+
toRepository: destinationImage.repository
152+
)
153+
}
154+
}
155+
156+
// MARK: Upload application manifest
157+
158+
let manifestDescriptor = try await destination.putManifest(
159+
repository: destinationImage.repository,
160+
reference: destinationImage.reference,
161+
manifest: manifest
162+
)
163+
164+
if verbose {
165+
log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)")
166+
}
167+
168+
// Use the manifest's digest if the user did not provide a human-readable tag
169+
// To support multiarch images, we should also create an an index pointing to
170+
// this manifest.
171+
let reference: ImageReference.Reference
172+
if let tag {
173+
reference = try ImageReference.Tag(tag)
174+
} else {
175+
reference = try ImageReference.Digest(manifestDescriptor.digest)
176+
}
177+
178+
var result = destinationImage
179+
result.reference = reference
180+
return result
181+
}

Sources/containertool/containertool.swift

Lines changed: 4 additions & 167 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,10 @@
1212
//
1313
//===----------------------------------------------------------------------===//
1414

15-
import ArgumentParser
1615
import Foundation
17-
import ContainerRegistry
18-
import Tar
16+
import ArgumentParser
1917
import Basics
18+
import ContainerRegistry
2019

2120
extension Swift.String: Swift.Error {}
2221

@@ -216,10 +215,11 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
216215

217216
// MARK: Build the image
218217

219-
let finalImage = try await destination.publishContainerImage(
218+
let finalImage = try await publishContainerImage(
220219
baseImage: baseImage,
221220
destinationImage: destinationImage,
222221
source: source,
222+
destination: destination,
223223
architecture: architecture,
224224
os: os,
225225
resources: imageBuildOptions.resources,
@@ -231,166 +231,3 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti
231231
print(finalImage)
232232
}
233233
}
234-
235-
extension RegistryClient {
236-
func publishContainerImage(
237-
baseImage: ImageReference,
238-
destinationImage: ImageReference,
239-
source: RegistryClient?,
240-
architecture: String,
241-
os: String,
242-
resources: [String],
243-
tag: String?,
244-
verbose: Bool,
245-
executableURL: URL
246-
) async throws -> ImageReference {
247-
248-
// MARK: Find the base image
249-
250-
let baseImageManifest: ImageManifest
251-
let baseImageConfiguration: ImageConfiguration
252-
let baseImageDescriptor: ContentDescriptor
253-
if let source {
254-
(baseImageManifest, baseImageDescriptor) = try await source.getImageManifest(
255-
forImage: baseImage,
256-
architecture: architecture
257-
)
258-
try log("Found base image manifest: \(ImageReference.Digest(baseImageDescriptor.digest))")
259-
260-
baseImageConfiguration = try await source.getImageConfiguration(
261-
forImage: baseImage,
262-
digest: ImageReference.Digest(baseImageManifest.config.digest)
263-
)
264-
log("Found base image configuration: \(baseImageManifest.config.digest)")
265-
} else {
266-
baseImageManifest = .init(
267-
schemaVersion: 2,
268-
config: .init(mediaType: "scratch", digest: "scratch", size: 0),
269-
layers: []
270-
)
271-
baseImageConfiguration = .init(
272-
architecture: architecture,
273-
os: os,
274-
rootfs: .init(_type: "layers", diff_ids: [])
275-
)
276-
if verbose { log("Using scratch as base image") }
277-
}
278-
279-
// MARK: Upload resource layers
280-
281-
var resourceLayers: [RegistryClient.ImageLayer] = []
282-
for resourceDir in resources {
283-
let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes
284-
let resourceLayer = try await self.uploadLayer(
285-
repository: destinationImage.repository,
286-
contents: resourceTardiff
287-
)
288-
289-
if verbose {
290-
log("resource layer: \(resourceLayer.descriptor.digest) (\(resourceLayer.descriptor.size) bytes)")
291-
}
292-
293-
resourceLayers.append(resourceLayer)
294-
}
295-
296-
// MARK: Upload the application layer
297-
298-
let applicationLayer = try await self.uploadLayer(
299-
repository: destinationImage.repository,
300-
contents: try Archive().appendingFile(at: executableURL).bytes
301-
)
302-
if verbose {
303-
log("application layer: \(applicationLayer.descriptor.digest) (\(applicationLayer.descriptor.size) bytes)")
304-
}
305-
306-
// MARK: Create the application configuration
307-
308-
let timestamp = Date(timeIntervalSince1970: 0).ISO8601Format()
309-
310-
// Inherit the configuration of the base image - UID, GID, environment etc -
311-
// and override the entrypoint.
312-
var inheritedConfiguration = baseImageConfiguration.config ?? .init()
313-
inheritedConfiguration.Entrypoint = ["/\(executableURL.lastPathComponent)"]
314-
inheritedConfiguration.Cmd = []
315-
inheritedConfiguration.WorkingDir = "/"
316-
317-
let configuration = ImageConfiguration(
318-
created: timestamp,
319-
architecture: architecture,
320-
os: os,
321-
config: inheritedConfiguration,
322-
rootfs: .init(
323-
_type: "layers",
324-
// The diff_id is the digest of the _uncompressed_ layer archive.
325-
// It is used by the runtime, which might not store the layers in
326-
// the compressed form in which it received them from the registry.
327-
diff_ids: baseImageConfiguration.rootfs.diff_ids
328-
+ resourceLayers.map { "\($0.diffID)" }
329-
+ ["\(applicationLayer.diffID)"]
330-
),
331-
history: [.init(created: timestamp, created_by: "containertool")]
332-
)
333-
334-
let configurationBlobReference = try await self.putImageConfiguration(
335-
forImage: destinationImage,
336-
configuration: configuration
337-
)
338-
339-
if verbose {
340-
log("image configuration: \(configurationBlobReference.digest) (\(configurationBlobReference.size) bytes)")
341-
}
342-
343-
// MARK: Create application manifest
344-
345-
let manifest = ImageManifest(
346-
schemaVersion: 2,
347-
mediaType: "application/vnd.oci.image.manifest.v1+json",
348-
config: configurationBlobReference,
349-
layers: baseImageManifest.layers
350-
+ resourceLayers.map { $0.descriptor }
351-
+ [applicationLayer.descriptor]
352-
)
353-
354-
// MARK: Upload base image
355-
356-
// Copy the base image layers to the destination repository
357-
// Layers could be checked and uploaded concurrently
358-
// This could also happen in parallel with the application image build
359-
if let source {
360-
for layer in baseImageManifest.layers {
361-
try await source.copyBlob(
362-
digest: ImageReference.Digest(layer.digest),
363-
fromRepository: baseImage.repository,
364-
toClient: self,
365-
toRepository: destinationImage.repository
366-
)
367-
}
368-
}
369-
370-
// MARK: Upload application manifest
371-
372-
let manifestDescriptor = try await self.putManifest(
373-
repository: destinationImage.repository,
374-
reference: destinationImage.reference,
375-
manifest: manifest
376-
)
377-
378-
if verbose {
379-
log("manifest: \(manifestDescriptor.digest) (\(manifestDescriptor.size) bytes)")
380-
}
381-
382-
// Use the manifest's digest if the user did not provide a human-readable tag
383-
// To support multiarch images, we should also create an an index pointing to
384-
// this manifest.
385-
let reference: ImageReference.Reference
386-
if let tag {
387-
reference = try ImageReference.Tag(tag)
388-
} else {
389-
reference = try ImageReference.Digest(manifestDescriptor.digest)
390-
}
391-
392-
var result = destinationImage
393-
result.reference = reference
394-
return result
395-
}
396-
}

0 commit comments

Comments
 (0)