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
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ Email is not required.
### Contributors

Aditya Ramani (adityaramani)
Alex Kennedy (solventak)
Danny Canter (dcantah)
Dmitry Kovba (dkovba)
Eric Ernst (egernst)
Expand Down
8 changes: 8 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let package = Package(
.library(name: "ContainerPlugin", targets: ["ContainerPlugin"]),
.library(name: "ContainerVersion", targets: ["ContainerVersion"]),
.library(name: "ContainerXPC", targets: ["ContainerXPC"]),
.library(name: "Metadata", targets: ["Metadata"]),
.library(name: "SocketForwarder", targets: ["SocketForwarder"]),
],
dependencies: [
Expand Down Expand Up @@ -169,6 +170,7 @@ let package = Package(
"ContainerNetworkService",
"ContainerVersion",
"ContainerXPC",
"ContainerPlugin",
],
path: "Sources/Helpers/NetworkVmnet"
),
Expand All @@ -180,6 +182,7 @@ let package = Package(
.product(name: "ContainerizationOS", package: "containerization"),
"ContainerPersistence",
"ContainerXPC",
"Metadata",
],
path: "Sources/Services/ContainerNetworkService"
),
Expand Down Expand Up @@ -258,6 +261,7 @@ let package = Package(
"ContainerNetworkService",
"ContainerPlugin",
"ContainerXPC",
"Metadata",
"TerminalProgress",
]
),
Expand Down Expand Up @@ -305,6 +309,10 @@ let package = Package(
.product(name: "Logging", package: "swift-log"),
]
),
.target(
name: "Metadata",
dependencies: []
),
.target(
name: "TerminalProgress",
dependencies: [
Expand Down
9 changes: 8 additions & 1 deletion Sources/ContainerClient/Core/ContainerConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@

import ContainerNetworkService
import ContainerizationOCI
import Foundation
import Metadata

public struct ContainerConfiguration: Sendable, Codable {
public struct ContainerConfiguration: Sendable, Codable, HasMetadata {
/// Identifier for the container.
public var id: String
/// Image used to create the container.
Expand Down Expand Up @@ -50,6 +52,8 @@ public struct ContainerConfiguration: Sendable, Codable {
public var virtualization: Bool = false
/// Enable SSH agent socket forwarding from host to container.
public var ssh: Bool = false
/// Metadata for the container.
public var metadata: ResourceMetadata

enum CodingKeys: String, CodingKey {
case id
Expand All @@ -68,6 +72,7 @@ public struct ContainerConfiguration: Sendable, Codable {
case runtimeHandler
case virtualization
case ssh
case metadata
}

/// Create a configuration from the supplied Decoder, initializing missing
Expand Down Expand Up @@ -105,6 +110,7 @@ public struct ContainerConfiguration: Sendable, Codable {
runtimeHandler = try container.decodeIfPresent(String.self, forKey: .runtimeHandler) ?? "container-runtime-linux"
virtualization = try container.decodeIfPresent(Bool.self, forKey: .virtualization) ?? false
ssh = try container.decodeIfPresent(Bool.self, forKey: .ssh) ?? false
metadata = try container.decodeIfPresent(ResourceMetadata.self, forKey: .metadata) ?? ResourceMetadata(createdAt: nil)
}

public struct DNSConfiguration: Sendable, Codable {
Expand Down Expand Up @@ -148,5 +154,6 @@ public struct ContainerConfiguration: Sendable, Codable {
self.id = id
self.image = image
self.initProcess = process
self.metadata = ResourceMetadata(createdAt: Date.now)
}
}
39 changes: 36 additions & 3 deletions Sources/ContainerClient/Core/Volume.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//

import Foundation
import Metadata

/// A named or anonymous volume that can be mounted in containers.
public struct Volume: Sendable, Codable, Equatable, Identifiable {
Expand All @@ -29,20 +30,21 @@ public struct Volume: Sendable, Codable, Equatable, Identifiable {
// The mount point of the volume on the host.
public var source: String
// Timestamp when the volume was created.
public var createdAt: Date
public var createdAt: Date?
// User-defined key/value metadata.
public var labels: [String: String]
// Driver-specific options.
public var options: [String: String]
// Size of the volume in bytes (optional).
public var sizeInBytes: UInt64?
// Metadata associated with the volume.
public var metadata: ResourceMetadata

public init(
name: String,
driver: String = "local",
format: String = "ext4",
source: String,
createdAt: Date = Date(),
labels: [String: String] = [:],
options: [String: String] = [:],
sizeInBytes: UInt64? = nil
Expand All @@ -51,10 +53,31 @@ public struct Volume: Sendable, Codable, Equatable, Identifiable {
self.driver = driver
self.format = format
self.source = source
self.createdAt = createdAt
self.createdAt = nil
self.labels = labels
self.options = options
self.sizeInBytes = sizeInBytes
self.metadata = ResourceMetadata(createdAt: Date.now)
}

public init(from decoder: Decoder) throws {
let volume = try decoder.container(keyedBy: CodingKeys.self)
self.name = try volume.decode(String.self, forKey: .name)
self.driver = try volume.decode(String.self, forKey: .driver)
self.format = try volume.decode(String.self, forKey: .format)
self.source = try volume.decode(String.self, forKey: .source)

// if createdAt is set (previous version of struct) then move into metadata, created at should be nil from here on out
self.createdAt = nil
if let createdAt = try volume.decodeIfPresent(Date.self, forKey: .createdAt) {
self.metadata = ResourceMetadata(createdAt: createdAt)
} else {
self.metadata = try volume.decodeIfPresent(ResourceMetadata.self, forKey: .metadata) ?? ResourceMetadata(createdAt: nil)
}

self.labels = try volume.decode([String: String].self, forKey: .labels)
self.options = try volume.decode([String: String].self, forKey: .options)
self.sizeInBytes = try volume.decodeIfPresent(UInt64.self, forKey: .sizeInBytes)
}
}

Expand All @@ -68,6 +91,16 @@ extension Volume {
}
}

// coalesce createdAt
extension Volume {
public var createdAtCoalesced: Date? {
if let createdAt = self.createdAt {
return createdAt
}
return self.metadata.createdAt
}
}

/// Error types for volume operations.
public enum VolumeError: Error, LocalizedError {
case volumeNotFound(String)
Expand Down
14 changes: 12 additions & 2 deletions Sources/ContainerCommands/Container/ContainerList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ extension Application {
}

private func createHeader() -> [[String]] {
[["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY"]]
[["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "CREATED"]]
}

private func printContainers(containers: [ClientContainer], format: ListFormat) throws {
Expand Down Expand Up @@ -88,7 +88,16 @@ extension Application {

extension ClientContainer {
fileprivate var asRow: [String] {
[
let createdAtString: String
if let createdAt = self.configuration.metadata.createdAt {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
createdAtString = formatter.string(from: createdAt)
} else {
createdAtString = "-"
}

return [
self.id,
self.configuration.image.reference,
self.configuration.platform.os,
Expand All @@ -97,6 +106,7 @@ extension ClientContainer {
self.networks.compactMap { try? CIDRAddress($0.address).address.description }.joined(separator: ","),
"\(self.configuration.resources.cpus)",
"\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
createdAtString,
]
}
}
Expand Down
26 changes: 21 additions & 5 deletions Sources/ContainerCommands/Network/NetworkList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension Application {
}

private func createHeader() -> [[String]] {
[["NETWORK", "STATE", "SUBNET"]]
[["NETWORK", "STATE", "SUBNET", "CREATED"]]
}

func printNetworks(networks: [NetworkState], format: ListFormat) throws {
Expand Down Expand Up @@ -80,10 +80,18 @@ extension Application {
extension NetworkState {
var asRow: [String] {
switch self {
case .created(_):
return [self.id, self.state, "none"]
case .running(_, let status):
return [self.id, self.state, status.address]
case .created(let config):
var createdAtFormatted = "-"
if let createdAt = config.metadata.createdAt {
createdAtFormatted = DateFormatter.metadataFormatter.string(from: createdAt)
}
return [self.id, self.state, "none", createdAtFormatted]
case .running(let config, let status):
var createdAtFormatted = "-"
if let createdAt = config.metadata.createdAt {
createdAtFormatted = DateFormatter.metadataFormatter.string(from: createdAt)
}
return [self.id, self.state, status.address, createdAtFormatted]
}
}
}
Expand All @@ -93,6 +101,7 @@ public struct PrintableNetwork: Codable {
let state: String
let config: NetworkConfiguration
let status: NetworkStatus?
let createdAt: String

public init(_ network: NetworkState) {
self.id = network.id
Expand All @@ -105,5 +114,12 @@ public struct PrintableNetwork: Codable {
self.config = config
self.status = status
}
if let createdAt = self.config.metadata.createdAt {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
self.createdAt = formatter.string(from: createdAt)
} else {
self.createdAt = "-"
}
}
}
9 changes: 7 additions & 2 deletions Sources/ContainerCommands/Volume/VolumeList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ extension Application.VolumeCommand {
}

private func createHeader() -> [[String]] {
[["NAME", "TYPE", "DRIVER", "OPTIONS"]]
[["NAME", "TYPE", "DRIVER", "OPTIONS", "CREATED"]]
}

func printVolumes(volumes: [Volume], format: Application.ListFormat) throws {
Expand All @@ -63,7 +63,7 @@ extension Application.VolumeCommand {

// Sort volumes by creation time (newest first)
let sortedVolumes = volumes.sorted { v1, v2 in
v1.createdAt > v2.createdAt
v1.createdAtCoalesced ?? Date.distantPast > v2.createdAtCoalesced ?? Date.distantPast
}

var rows = createHeader()
Expand All @@ -81,11 +81,16 @@ extension Volume {
var asRow: [String] {
let volumeType = self.isAnonymous ? "anonymous" : "named"
let optionsString = options.isEmpty ? "" : options.map { "\($0.key)=\($0.value)" }.joined(separator: ",")
var createdAtStr = "-"
if let createdAt = self.metadata.createdAt {
createdAtStr = DateFormatter.metadataFormatter.string(from: createdAt)
}
return [
self.name,
volumeType,
self.driver,
optionsString,
createdAtStr,
]
}
}
17 changes: 16 additions & 1 deletion Sources/Helpers/NetworkVmnet/NetworkVmnetHelper+Start.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

import ArgumentParser
import ContainerNetworkService
import ContainerPersistence
import ContainerPlugin
import ContainerXPC
import ContainerizationExtras
import Foundation
Expand All @@ -40,6 +42,13 @@ extension NetworkVmnetHelper {
@Option(name: .shortAndLong, help: "CIDR address for the subnet")
var subnet: String?

func loadNetworkConfiguration(id: String, log: Logger) async throws -> NetworkConfiguration? {
let appRoot = ApplicationRoot.url
let resourceRoot = appRoot.appendingPathComponent("networks")
let store = try FilesystemEntityStore<NetworkConfiguration>(path: resourceRoot, type: "network", log: log)
return try await store.retrieve(id)
}

func run() async throws {
let commandName = NetworkVmnetHelper._commandName
let log = setupLogger(id: id, debug: debug)
Expand All @@ -51,7 +60,13 @@ extension NetworkVmnetHelper {
do {
log.info("configuring XPC server")
let subnet = try self.subnet.map { try CIDRAddress($0) }
let configuration = try NetworkConfiguration(id: id, mode: .nat, subnet: subnet?.description)
let configuration =
try await loadNetworkConfiguration(id: id, log: log)
?? NetworkConfiguration(
id: id,
mode: .nat,
subnet: subnet?.description
)
let network = try Self.createNetwork(configuration: configuration, log: log)
try await network.start()
let server = try await NetworkService(network: network, log: log)
Expand Down
41 changes: 41 additions & 0 deletions Sources/Metadata/Metadata.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//===----------------------------------------------------------------------===//
// Copyright © 2025 Apple Inc. and the container project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//

import Foundation

/// Metadata fields for resources.
public struct ResourceMetadata: Sendable, Codable, Equatable {
/// Creation timestamp for the resource.
public var createdAt: Date?

public init(createdAt: Date?) {
self.createdAt = createdAt
}
}

/// Protocol for resources that have common metadata fields.
public protocol HasMetadata {
var metadata: ResourceMetadata { get set }
}

extension DateFormatter {
/// A date formatter for ISO 8601 dates with fractional seconds.
public static var metadataFormatter: ISO8601DateFormatter {
let formatter = ISO8601DateFormatter()
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
return formatter
}
}
Loading