Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions libs/lume/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,14 @@ let package = Package(
.package(url: "https://github.com/apple/swift-nio-ssh.git", from: "0.12.0")
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.systemLibrary(
name: "CZlib",
path: "Sources/CZlib"
),
.executableTarget(
name: "lume",
dependencies: [
"CZlib",
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Atomics", package: "swift-atomics"),
.product(name: "Dynamic", package: "Dynamic"),
Expand Down
5 changes: 5 additions & 0 deletions libs/lume/Sources/CZlib/module.modulemap
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module CZlib [system] {
header "shim.h"
link "z"
export *
}
1 change: 1 addition & 0 deletions libs/lume/Sources/CZlib/shim.h
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#include <zlib.h>
119 changes: 119 additions & 0 deletions libs/lume/src/Commands/Convert.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import ArgumentParser
import Foundation

struct Convert: AsyncParsableCommand {
static let configuration = CommandConfiguration(
abstract: "Convert a legacy Lume image to OCI-compliant format",
discussion: """
Pulls a legacy Lume image from the registry, re-pushes it in OCI-compliant format
under a new name/tag, then removes the temporary local VM.

Example:
lume convert macos-sequoia:latest trycua/macos-sequoia:latest-oci
"""
)

@Argument(help: "Source image to convert (legacy format, e.g. macos-sequoia:latest)")
var sourceImage: String

@Argument(help: "Target image to push in OCI format (format: name:tag)")
var targetImage: String

@Option(parsing: .upToNextOption, help: "Additional tags to push the OCI image to")
var additionalTags: [String] = []

@Option(help: "Registry to pull from and push to. Defaults to ghcr.io")
var registry: String = "ghcr.io"

@Option(help: "Organization. Defaults to trycua")
var organization: String = "trycua"

@Flag(name: .long, help: "Enable verbose logging")
var verbose: Bool = false

@Flag(name: .long, help: "Prepare files without uploading to registry")
var dryRun: Bool = false

@Flag(name: .long, help: "Push as a single disk layer (kubelet-compatible, no chunking)")
var singleLayer: Bool = false

init() {}

@MainActor
func run() async throws {
if verbose { Logger.setVerbose() }

TelemetryClient.shared.record(event: TelemetryEvent.push)

let targetComponents = targetImage.split(separator: ":")
guard targetComponents.count == 2, let primaryTag = targetComponents.last else {
throw ValidationError("Invalid target image format. Expected format: name:tag")
}
let targetName = String(targetComponents.first!)

var allTags: Swift.Set<String> = [String(primaryTag)]
allTags.formUnion(additionalTags)

// Use a unique temp VM name to avoid collisions
let tempVMName = "__lume_convert_\(UUID().uuidString.prefix(8))"

let controller = LumeController()

Logger.debug(
"Converting legacy image to OCI-compliant format",
metadata: [
"source": sourceImage,
"target": targetImage,
"registry": registry,
"organization": organization,
])

// Step 1: Pull the legacy source image into a temp VM
Logger.info("Step 1/3: Pulling source image '\(sourceImage)'…")
do {
try await controller.pullImage(
image: sourceImage,
name: tempVMName,
registry: registry,
organization: organization
)
} catch {
Logger.error("Pull failed, cleaning up temp VM: \(error.localizedDescription)")
try? await controller.delete(name: tempVMName)
throw error
}

// Step 2: Push the temp VM in OCI-compliant format
Logger.info("Step 2/3: Pushing '\(sourceImage)' as OCI-compliant '\(targetImage)'…")
do {
try await controller.pushImage(
name: tempVMName,
imageName: targetName,
tags: Array(allTags),
registry: registry,
organization: organization,
verbose: verbose,
dryRun: dryRun,
reassemble: false,
singleLayer: singleLayer,
legacy: false // OCI-compliant output
)
} catch {
// Always clean up the temp VM even if push fails
Logger.error("Push failed, cleaning up temp VM: \(error.localizedDescription)")
try? await controller.delete(name: tempVMName)
throw error
}

// Step 3: Remove the temp VM
Logger.info("Step 3/3: Cleaning up temporary VM…")
try await controller.delete(name: tempVMName)

Logger.info(
"Conversion complete",
metadata: [
"source": sourceImage,
"target": targetImage,
])
}
}
5 changes: 5 additions & 0 deletions libs/lume/src/Commands/Pull.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@ struct Pull: AsyncParsableCommand {
@Option(name: .customLong("storage"), help: "VM storage location to use or direct path to VM location")
var storage: String?

@Flag(name: .long, help: "Enable verbose logging")
var verbose: Bool = false

init() {}

@MainActor
func run() async throws {
if verbose { Logger.setVerbose() }

// Record telemetry - only capture image name without tag for privacy
let imageName = image.split(separator: ":").first.map(String.init) ?? image
TelemetryClient.shared.record(event: TelemetryEvent.pull, properties: [
Expand Down
18 changes: 16 additions & 2 deletions libs/lume/src/Commands/Push.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,18 @@ struct Push: AsyncParsableCommand {
@Flag(name: .long, help: "In dry-run mode, also reassemble chunks to verify integrity")
var reassemble: Bool = true

@Flag(name: .long, help: "Push as a single disk layer (kubelet-compatible, no chunking)")
var singleLayer: Bool = false

@Flag(name: .long, help: "Use legacy Lume LZ4-chunked format instead of OCI-compliant format")
var legacy: Bool = false

init() {}

@MainActor
func run() async throws {
if verbose { Logger.setVerbose() }

// Record telemetry
TelemetryClient.shared.record(event: TelemetryEvent.push)

Expand All @@ -60,7 +68,11 @@ struct Push: AsyncParsableCommand {
guard !allTags.isEmpty else {
throw ValidationError("At least one tag must be provided.")
}


if singleLayer && legacy {
throw ValidationError("--single-layer and --legacy are mutually exclusive.")
}

try await controller.pushImage(
name: name,
imageName: imageName, // Pass base image name
Expand All @@ -71,7 +83,9 @@ struct Push: AsyncParsableCommand {
chunkSizeMb: chunkSizeMb,
verbose: verbose,
dryRun: dryRun,
reassemble: reassemble
reassemble: reassemble,
singleLayer: singleLayer,
legacy: legacy
)
}
}
4 changes: 3 additions & 1 deletion libs/lume/src/ContainerRegistry/GCSImageRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ class GCSImageRegistry: ImageRegistry, @unchecked Sendable {
chunkSizeMb: Int,
verbose: Bool,
dryRun: Bool,
reassemble: Bool
reassemble: Bool,
singleLayer: Bool,
legacy: Bool
) async throws {
guard !tags.isEmpty else {
throw GCSRegistryError.noTagsProvided
Expand Down
Loading
Loading