diff --git a/.spi.yml b/.spi.yml index b2194b2..baaed12 100644 --- a/.spi.yml +++ b/.spi.yml @@ -2,5 +2,4 @@ version: 1 builder: configs: - documentation_targets: - - containertool - swift-container-plugin diff --git a/Examples/HelloWorldHummingbird/Package.swift b/Examples/HelloWorldHummingbird/Package.swift index 8361bc4..e4deb8e 100644 --- a/Examples/HelloWorldHummingbird/Package.swift +++ b/Examples/HelloWorldHummingbird/Package.swift @@ -21,7 +21,7 @@ let package = Package( platforms: [.macOS(.v14)], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.1.0"), - .package(url: "https://github.com/apple/swift-container-plugin", from: "0.5.0"), + .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), ], targets: [ diff --git a/Examples/HelloWorldVapor/Package.swift b/Examples/HelloWorldVapor/Package.swift index 6f0bcd4..f718889 100644 --- a/Examples/HelloWorldVapor/Package.swift +++ b/Examples/HelloWorldVapor/Package.swift @@ -21,7 +21,7 @@ let package = Package( platforms: [.macOS(.v13)], dependencies: [ .package(url: "https://github.com/vapor/vapor", from: "4.102.0"), - .package(url: "https://github.com/apple/swift-container-plugin", from: "0.5.0"), + .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), ], targets: [.executableTarget(name: "hello-world", dependencies: [.product(name: "Vapor", package: "vapor")])] ) diff --git a/Examples/HelloWorldWithResources/Package.swift b/Examples/HelloWorldWithResources/Package.swift index 17e387f..62307bf 100644 --- a/Examples/HelloWorldWithResources/Package.swift +++ b/Examples/HelloWorldWithResources/Package.swift @@ -21,7 +21,7 @@ let package = Package( platforms: [.macOS(.v14)], dependencies: [ .package(url: "https://github.com/hummingbird-project/hummingbird.git", from: "2.1.0"), - .package(url: "https://github.com/apple/swift-container-plugin", from: "0.5.0"), + .package(url: "https://github.com/apple/swift-container-plugin", from: "1.0.0"), .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), ], targets: [ diff --git a/README.md b/README.md index 8e0c932..c2ebcee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Swift Container Plugin -[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/apple/swift-container-plugin/documentation/containerimagebuilderplugin) +[![](https://img.shields.io/badge/docc-read_documentation-blue)](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin) [![](https://img.shields.io/github/v/release/apple/swift-container-plugin?include_prereleases)](https://github.com/apple/swift-container-plugin/releases) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-container-plugin%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/apple/swift-container-plugin) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fapple%2Fswift-container-plugin%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/apple/swift-container-plugin) @@ -12,12 +12,18 @@ Build and publish container images using Swift Package Manager. Container images are the standard way to package cloud software today. Once you have packaged your server in a container image, you can deploy it on any container-based public or private cloud service, or run it locally using a desktop container runtime. -Swift Container Plugin makes it easy to build container images for servers written in Swift, using Swift Package Manager. +Use Swift Container Plugin to build and publish container images for your Swift services in one streamlined workflow with Swift Package Manager. -Find out more and see it in action: +![Swift Container Plugin flow diagram](Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png) -* [How to put Swift in a box](https://fosdem.org/2025/schedule/event/fosdem-2025-5116-how-to-put-swift-in-a-box-building-container-images-with-swift-container-plugin/) at [FOSDEM 2025](https://fosdem.org/2025/schedule/track/swift/). -* [Swift to the cloud in a single step](https://www.youtube.com/watch?v=9AaINsCfZzw) at [ServerSide.Swift 2024](https://www.serversideswift.info/2024/speakers/euan-harris/). +1. [Add the plugin to your project's dependencies](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin/adding-the-plugin-to-your-project) in `Package.swift`. +2. [Build and package your service](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin/build) using Swift Package Manager. + - If you are building on macOS, [use a Swift SDK](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin/requirements) to cross-compile a Linux executable. + - If you are building on Linux, use your native Swift compiler to build a Linux executable. If you have special requirements such as building a static executable, or cross-compiling to a different processor architecture, use a suitable Swift SDK. +3. The plugin automatically packages your executable in a container image and publishes it to your chosen container registry. +4. [Run your container image](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin/run) on any container-based platform. + +Find full details [in the documentation](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin). ## Usage @@ -25,7 +31,8 @@ Swift Container Plugin can package any executable product defined in `Package.sw ### Build and publish a container image -After adding the plugin to your project, you can build and publish a container image in one step: +After adding the plugin to your project, you can build and publish a container image in one step. +Here is how to build the [HelloWorld example](https://github.com/apple/swift-container-plugin/tree/main/Examples/HelloWorldHummingbird) as a static executable for Linux running on the `x86_64` architecture: ``` % swift package --swift-sdk x86_64-swift-linux-musl \ @@ -52,7 +59,7 @@ registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed919 ### Run the image -You can deploy your service in the cloud, or use a standards-compliant container runtime such as `podman` to run it locally: +Deploy your service in the cloud, or use a standards-compliant container runtime to run it locally: ``` % podman run -p 8080:8080 registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df @@ -67,8 +74,12 @@ Trying to pull registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c904 * On macOS you must install a cross-compilation Swift SDK, such as the [Swift Static Linux SDK](https://www.swift.org/documentation/articles/static-linux-getting-started.html), in order to build executables which can run on Linux-based cloud infrastructure. * A container runtime is not required to build an image, but one must be available wherever the image is to be run. -## Getting Started +### Find out more +* Learn more about setting up your project in the [plugin documentation](https://swiftpackageindex.com/apple/swift-container-plugin/main/documentation/swift-container-plugin). + +* Take a look at [more examples](https://github.com/apple/swift-container-plugin/tree/main/Examples). -Learn more about setting up your project in the [plugin documentation](https://swiftpackageindex.com/apple/swift-container-plugin/documentation/containerimagebuilderplugin). +* Watch some talks: -Take a look at the [Examples](Examples). + * [How to put Swift in a box](https://fosdem.org/2025/schedule/event/fosdem-2025-5116-how-to-put-swift-in-a-box-building-container-images-with-swift-container-plugin/) at [FOSDEM 2025](https://fosdem.org/2025/schedule/track/swift/). + * [Swift to the cloud in a single step](https://www.youtube.com/watch?v=9AaINsCfZzw) at [ServerSide.Swift 2024](https://www.serversideswift.info/2024/speakers/euan-harris/). \ No newline at end of file diff --git a/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift b/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift index 939c42c..64e141d 100644 --- a/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift +++ b/Sources/ContainerRegistry/RegistryClient+ImageConfiguration.swift @@ -15,29 +15,30 @@ extension RegistryClient { /// Get an image configuration record from the registry. /// - Parameters: - /// - repository: Name of the repository containing the record. + /// - image: Reference to the image containing the record. /// - digest: Digest of the record. /// - Returns: The image confguration record stored in `repository` with digest `digest`. /// - Throws: If the blob cannot be decoded as an `ImageConfiguration`. /// /// Image configuration records are stored as blobs in the registry. This function retrieves the requested blob and tries to decode it as a configuration record. - public func getImageConfiguration(repository: String, digest: String) async throws -> ImageConfiguration { - try await getBlob(repository: repository, digest: digest) + public func getImageConfiguration(forImage image: ImageReference, digest: String) async throws -> ImageConfiguration + { + try await getBlob(repository: image.repository, digest: digest) } /// Upload an image configuration record to the registry. /// - Parameters: - /// - repository: Name of the repository in which to store the record. + /// - image: Reference to the image associated with the record. /// - configuration: An image configuration record /// - Returns: An `ContentDescriptor` referring to the blob stored in the registry. /// - Throws: If the blob upload fails. /// /// Image configuration records are stored as blobs in the registry. This function encodes the provided configuration record and stores it as a blob in the registry. - public func putImageConfiguration(repository: String, configuration: ImageConfiguration) async throws + public func putImageConfiguration(forImage image: ImageReference, configuration: ImageConfiguration) async throws -> ContentDescriptor { try await putBlob( - repository: repository, + repository: image.repository, mediaType: "application/vnd.oci.image.config.v1+json", data: configuration ) diff --git a/Sources/containertool/Extensions/RegistryClient+Layers.swift b/Sources/containertool/Extensions/RegistryClient+Layers.swift index d6d344d..ccc79b1 100644 --- a/Sources/containertool/Extensions/RegistryClient+Layers.swift +++ b/Sources/containertool/Extensions/RegistryClient+Layers.swift @@ -16,23 +16,23 @@ import struct Foundation.Data import ContainerRegistry extension RegistryClient { - func getImageManifest(repository: String, reference: String, architecture: String) async throws -> ImageManifest { + func getImageManifest(forImage image: ImageReference, architecture: String) async throws -> ImageManifest { // We pushed the amd64 tag but it points to a single-architecture index, not directly to a manifest // if we get an index we should get a manifest, otherwise we might get a manifest directly do { // Try to retrieve a manifest. If the object with this reference is actually an index, the content-type will not match and // an error will be thrown. - return try await getManifest(repository: repository, reference: reference) + return try await getManifest(repository: image.repository, reference: image.reference) } catch { // Try again, treating the top level object as an index. // This could be more efficient if the exception thrown by getManfiest() included the data it was unable to parse - let index = try await getIndex(repository: repository, reference: reference) + let index = try await getIndex(repository: image.repository, reference: image.reference) guard let manifest = index.manifests.first(where: { $0.platform?.architecture == architecture }) else { throw "Could not find a suitable base image for \(architecture)" } // The index should not point to another index; if it does, this call will throw a final error to be handled by the caller. - return try await getManifest(repository: repository, reference: manifest.digest) + return try await getManifest(repository: image.repository, reference: manifest.digest) } } diff --git a/Sources/containertool/containertool.swift b/Sources/containertool/containertool.swift index db19ea1..d69f6e4 100644 --- a/Sources/containertool/containertool.swift +++ b/Sources/containertool/containertool.swift @@ -29,8 +29,7 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti ) @Option(help: "Default registry for references which do not specify a registry") - private var defaultRegistry: String = - ProcessInfo.processInfo.environment["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io" + private var defaultRegistry: String? @Option(help: "Repository path") private var repository: String @@ -51,16 +50,16 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti private var verbose: Bool = false @Option(help: "Connect to the container registry using plaintext HTTP") - var allowInsecureHttp: AllowHTTP? + private var allowInsecureHttp: AllowHTTP? @Option(help: "CPU architecture") private var architecture: String? @Option(help: "Base image reference") - private var from: String = ProcessInfo.processInfo.environment["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim" + private var from: String? @Option(help: "Operating system") - private var os: String = ProcessInfo.processInfo.environment["CONTAINERTOOL_OS"] ?? "linux" + private var os: String? @Option(help: "Tag for this manifest") private var tag: String? @@ -72,8 +71,26 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti private var netrcFile: String? func run() async throws { - let baseimage = try ImageReference(fromString: from, defaultRegistry: defaultRegistry) - var destination_image = try ImageReference(fromString: repository, defaultRegistry: defaultRegistry) + // MARK: Apply defaults for unspecified configuration flags + + let env = ProcessInfo.processInfo.environment + let defaultRegistry = defaultRegistry ?? env["CONTAINERTOOL_DEFAULT_REGISTRY"] ?? "docker.io" + let from = from ?? env["CONTAINERTOOL_BASE_IMAGE"] ?? "swift:slim" + let os = os ?? env["CONTAINERTOOL_OS"] ?? "linux" + + // Try to detect the architecture of the application executable so a suitable base image can be selected. + // This reduces the risk of accidentally creating an image which stacks an aarch64 executable on top of an x86_64 base image. + let executableURL = URL(fileURLWithPath: executable) + let elfheader = try ELF.read(at: executableURL) + + let architecture = + architecture + ?? env["CONTAINERTOOL_ARCHITECTURE"] + ?? elfheader?.ISA.containerArchitecture + ?? "amd64" + if verbose { log("Base image architecture: \(architecture)") } + + // MARK: Load netrc let authProvider: AuthorizationProvider? if !netrc { @@ -89,6 +106,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti // MARK: Create registry clients + let baseImage = try ImageReference(fromString: from, defaultRegistry: defaultRegistry) + let destinationImage = try ImageReference(fromString: repository, defaultRegistry: defaultRegistry) + // The base image may be stored on a different registry to the final destination, so two clients are needed. // `scratch` is a special case and requires no source client. let source: RegistryClient? @@ -96,57 +116,80 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti source = nil } else { source = try await RegistryClient( - registry: baseimage.registry, + registry: baseImage.registry, insecure: allowInsecureHttp == .source || allowInsecureHttp == .both, auth: .init(username: username, password: password, auth: authProvider) ) - if verbose { log("Connected to source registry: \(baseimage.registry)") } + if verbose { log("Connected to source registry: \(baseImage.registry)") } } let destination = try await RegistryClient( - registry: destination_image.registry, + registry: destinationImage.registry, insecure: allowInsecureHttp == .destination || allowInsecureHttp == .both, auth: .init(username: username, password: password, auth: authProvider) ) - if verbose { log("Connected to destination registry: \(destination_image.registry)") } - if verbose { log("Using base image: \(baseimage)") } + if verbose { log("Connected to destination registry: \(destinationImage.registry)") } + if verbose { log("Using base image: \(baseImage)") } - // MARK: Find the base image + // MARK: Build the image - // Try to detect the architecture of the application executable so a suitable base image can be selected. - // This reduces the risk of accidentally creating an image which stacks an aarch64 executable on top of an x86_64 base image. - let executableURL = URL(fileURLWithPath: executable) - let elfheader = try ELF.read(at: executableURL) - let architecture = - architecture - ?? ProcessInfo.processInfo.environment["CONTAINERTOOL_ARCHITECTURE"] - ?? elfheader?.ISA.containerArchitecture - ?? "amd64" - if verbose { log("Base image architecture: \(architecture)") } + let finalImage = try await destination.publishContainerImage( + baseImage: baseImage, + destinationImage: destinationImage, + source: source, + architecture: architecture, + os: os, + resources: resources, + tag: tag, + verbose: verbose, + executableURL: executableURL + ) - let baseimage_manifest: ImageManifest - let baseimage_config: ImageConfiguration + print(finalImage) + } +} + +extension RegistryClient { + func publishContainerImage( + baseImage: ImageReference, + destinationImage: ImageReference, + source: RegistryClient?, + architecture: String, + os: String, + resources: [String], + tag: String?, + verbose: Bool, + executableURL: URL + ) async throws -> ImageReference { + + // MARK: Find the base image + + let baseImageManifest: ImageManifest + let baseImageConfiguration: ImageConfiguration if let source { - baseimage_manifest = try await source.getImageManifest( - repository: baseimage.repository, - reference: baseimage.reference, + baseImageManifest = try await source.getImageManifest( + forImage: baseImage, architecture: architecture ) - log("Found base image manifest: \(baseimage_manifest.digest)") + log("Found base image manifest: \(baseImageManifest.digest)") - baseimage_config = try await source.getImageConfiguration( - repository: baseimage.repository, - digest: baseimage_manifest.config.digest + baseImageConfiguration = try await source.getImageConfiguration( + forImage: baseImage, + digest: baseImageManifest.config.digest ) - log("Found base image configuration: \(baseimage_manifest.config.digest)") + log("Found base image configuration: \(baseImageManifest.config.digest)") } else { - baseimage_manifest = .init( + baseImageManifest = .init( schemaVersion: 2, config: .init(mediaType: "scratch", digest: "scratch", size: 0), layers: [] ) - baseimage_config = .init(architecture: architecture, os: os, rootfs: .init(_type: "layers", diff_ids: [])) + baseImageConfiguration = .init( + architecture: architecture, + os: os, + rootfs: .init(_type: "layers", diff_ids: []) + ) if verbose { log("Using scratch as base image") } } @@ -155,8 +198,8 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti var resourceLayers: [RegistryClient.ImageLayer] = [] for resourceDir in resources { let resourceTardiff = try Archive().appendingRecursively(atPath: resourceDir).bytes - let resourceLayer = try await destination.uploadLayer( - repository: destination_image.repository, + let resourceLayer = try await self.uploadLayer( + repository: destinationImage.repository, contents: resourceTardiff ) @@ -168,8 +211,9 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti } // MARK: Upload the application layer - let applicationLayer = try await destination.uploadLayer( - repository: destination_image.repository, + + let applicationLayer = try await self.uploadLayer( + repository: destinationImage.repository, contents: try Archive().appendingFile(at: executableURL).bytes ) if verbose { @@ -177,46 +221,49 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti } // MARK: Create the application configuration + let timestamp = Date(timeIntervalSince1970: 0).ISO8601Format() // Inherit the configuration of the base image - UID, GID, environment etc - // and override the entrypoint. - var inherited_config = baseimage_config.config ?? .init() - inherited_config.Entrypoint = ["/\(executableURL.lastPathComponent)"] - inherited_config.Cmd = [] - inherited_config.WorkingDir = "/" + var inheritedConfiguration = baseImageConfiguration.config ?? .init() + inheritedConfiguration.Entrypoint = ["/\(executableURL.lastPathComponent)"] + inheritedConfiguration.Cmd = [] + inheritedConfiguration.WorkingDir = "/" let configuration = ImageConfiguration( created: timestamp, architecture: architecture, os: os, - config: inherited_config, + config: inheritedConfiguration, rootfs: .init( _type: "layers", // The diff_id is the digest of the _uncompressed_ layer archive. // It is used by the runtime, which might not store the layers in // the compressed form in which it received them from the registry. - diff_ids: baseimage_config.rootfs.diff_ids + diff_ids: baseImageConfiguration.rootfs.diff_ids + resourceLayers.map { $0.diffID } + [applicationLayer.diffID] ), history: [.init(created: timestamp, created_by: "containertool")] ) - let config_blob = try await destination.putImageConfiguration( - repository: destination_image.repository, + let configurationBlobReference = try await self.putImageConfiguration( + forImage: destinationImage, configuration: configuration ) - if verbose { log("image configuration: \(config_blob.digest) (\(config_blob.size) bytes)") } + if verbose { + log("image configuration: \(configurationBlobReference.digest) (\(configurationBlobReference.size) bytes)") + } // MARK: Create application manifest let manifest = ImageManifest( schemaVersion: 2, mediaType: "application/vnd.oci.image.manifest.v1+json", - config: config_blob, - layers: baseimage_manifest.layers + config: configurationBlobReference, + layers: baseImageManifest.layers + resourceLayers.map { $0.descriptor } + [applicationLayer.descriptor] ) @@ -227,12 +274,12 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti // Layers could be checked and uploaded concurrently // This could also happen in parallel with the application image build if let source { - for layer in baseimage_manifest.layers { + for layer in baseImageManifest.layers { try await source.copyBlob( digest: layer.digest, - fromRepository: baseimage.repository, - toClient: destination, - toRepository: destination_image.repository + fromRepository: baseImage.repository, + toClient: self, + toRepository: destinationImage.repository ) } } @@ -243,15 +290,16 @@ enum AllowHTTP: String, ExpressibleByArgument, CaseIterable { case source, desti // To support multiarch images, we should also create an an index pointing to // this manifest. let reference = tag ?? manifest.digest - let location = try await destination.putManifest( - repository: destination_image.repository, - reference: destination_image.reference, + let location = try await self.putManifest( + repository: destinationImage.repository, + reference: destinationImage.reference, manifest: manifest ) if verbose { log(location) } - destination_image.reference = reference - print(destination_image) + var result = destinationImage + result.reference = reference + return result } } diff --git a/Sources/swift-container-plugin/Documentation.docc/Swift-Container-Plugin.md b/Sources/swift-container-plugin/Documentation.docc/Swift-Container-Plugin.md index 1bf8326..8b83748 100644 --- a/Sources/swift-container-plugin/Documentation.docc/Swift-Container-Plugin.md +++ b/Sources/swift-container-plugin/Documentation.docc/Swift-Container-Plugin.md @@ -10,26 +10,25 @@ Build and publish container images using Swift Package Manager. Container images are the standard way to package cloud software today. Once you have packaged your server in a container image, you can deploy it on any container-based public or private cloud service, or run it locally using a desktop container runtime. -Swift Container Plugin makes it easy to build container images for servers written in Swift, using Swift Package Manager. +Use Swift Container Plugin to build and publish container images for your Swift services in one streamlined workflow with Swift Package Manager. -Find out more and see it in action: +![Swift Container Plugin flow diagram](swift-container-plugin-flow-diagram) -* [How to put Swift in a box](https://fosdem.org/2025/schedule/event/fosdem-2025-5116-how-to-put-swift-in-a-box-building-container-images-with-swift-container-plugin/) at [FOSDEM 2025](https://fosdem.org/2025/schedule/track/swift/). -* [Swift to the cloud in a single step](https://www.youtube.com/watch?v=9AaINsCfZzw) at [ServerSide.Swift 2024](https://www.serversideswift.info/2024/speakers/euan-harris/). +1. [Add the plugin to your project's dependencies](doc:Adding-the-plugin-to-your-project) in `Package.swift`. +2. [Build and package your service](doc:build) using Swift Package Manager. + - If you are building on macOS, [use a Swift SDK](doc:requirements) to cross-compile a Linux executable. + - If you are building on Linux, use your native Swift compiler to build a Linux executable. If you have special requirements such as building a static executable, or cross-compiling to a different processor architecture, use a suitable Swift SDK. +3. The plugin automatically packages your executable in a container image and publishes it to your chosen container registry. +4. [Run your container image](doc:run) on any container-based platform. ### Usage -Use the Swift Container Plugin to package an executable product from your Swift package into a container image and publish it to a container registry. +Swift Container Plugin can package any executable product defined in `Package.swift`. -To use the plugin: -- -- -- - -### Build and publish a container image +#### Build and publish a container image After adding the plugin to your project, you can build and publish a container image in one step. -Here is how to build the [HelloWorld example](https://github.com/apple/swift-container-plugin/tree/main/Examples/HelloWorldHummingbird) as a static executable for Linux running the `x86_64` architecture: +Here is how to build the [HelloWorld example](https://github.com/apple/swift-container-plugin/tree/main/Examples/HelloWorldHummingbird) as a static executable for Linux running on the `x86_64` architecture: ``` % swift package --swift-sdk x86_64-swift-linux-musl \ @@ -54,9 +53,9 @@ Build of product 'hello-world' complete! (5.51s) registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df ``` -### Run the image +#### Run the image -You can deploy your service in the cloud, or use a standards-compliant container runtime such as `podman` to run it locally: +Deploy your service in the cloud, or use a standards-compliant container runtime to run it locally: ``` % podman run -p 8080:8080 registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c9040b16f9f2c2ed9190317147dee95a218faf1df @@ -65,7 +64,14 @@ Trying to pull registry.example.com/myservice@sha256:a3f75d0932d052dd9d448a1c904 2024-05-26T22:57:50+0000 info HummingBird : [HummingbirdCore] Server started and listening on 0.0.0.0:8080 ``` -Take a look at some [other examples](https://github.com/apple/swift-container-plugin/tree/main/Examples). +### Find out more + +* Take a look at [more examples](https://github.com/apple/swift-container-plugin/tree/main/Examples). + +* Watch some talks: + + * [How to put Swift in a box](https://fosdem.org/2025/schedule/event/fosdem-2025-5116-how-to-put-swift-in-a-box-building-container-images-with-swift-container-plugin/) at [FOSDEM 2025](https://fosdem.org/2025/schedule/track/swift/). + * [Swift to the cloud in a single step](https://www.youtube.com/watch?v=9AaINsCfZzw) at [ServerSide.Swift 2024](https://www.serversideswift.info/2024/speakers/euan-harris/). ## Topics diff --git a/Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png b/Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png new file mode 100644 index 0000000..cb8b121 Binary files /dev/null and b/Sources/swift-container-plugin/Documentation.docc/_Resources/swift-container-plugin-flow-diagram.png differ diff --git a/Sources/swift-container-plugin/Documentation.docc/build-container-image.md b/Sources/swift-container-plugin/Documentation.docc/build-container-image.md index 66a5a9a..a28e449 100644 --- a/Sources/swift-container-plugin/Documentation.docc/build-container-image.md +++ b/Sources/swift-container-plugin/Documentation.docc/build-container-image.md @@ -34,16 +34,16 @@ Wrap a binary in a container image and publish it. If the `product` being packaged has a [resource bundle](https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package) it will be added to the image automatically. - term `--username `: - Username to use when logging into the registry. + Default username to use when logging into the registry. + This username is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. The same username is used for the source and destination registries. - The `.netrc` file is ignored when this option is specified. - term `--password `: - Password to use when logging into the registry. + Default password to use when logging into the registry. + This password is used if there is no matching `.netrc` entry for the registry, there is no `.netrc` file, or the `--disable-netrc` option is set. The same password is used for the source and destination registries. - The `.netrc` file is ignored when this option is specified. - term `-v, --verbose`: Verbose output. diff --git a/Tests/ContainerRegistryTests/SmokeTests.swift b/Tests/ContainerRegistryTests/SmokeTests.swift index 5969d04..d94c7a6 100644 --- a/Tests/ContainerRegistryTests/SmokeTests.swift +++ b/Tests/ContainerRegistryTests/SmokeTests.swift @@ -185,6 +185,7 @@ struct SmokeTests { @Test func testPutAndGetImageConfiguration() async throws { let repository = "testputandgetimageconfiguration" // repository name must be lowercase + let image = ImageReference(registry: "registry", repository: repository, reference: "latest") let configuration = ImageConfiguration( created: "1996-12-19T16:39:57-08:00", @@ -195,12 +196,12 @@ struct SmokeTests { history: [.init(created: "1996-12-19T16:39:57-08:00", author: "test", created_by: "smoketest")] ) let config_descriptor = try await client.putImageConfiguration( - repository: repository, + forImage: image, configuration: configuration ) let downloaded = try await client.getImageConfiguration( - repository: repository, + forImage: image, digest: config_descriptor.digest )