diff --git a/Sources/CLI/Registry/RegistryCommand.swift b/Sources/CLI/Registry/RegistryCommand.swift index c160c946..c946bb87 100644 --- a/Sources/CLI/Registry/RegistryCommand.swift +++ b/Sources/CLI/Registry/RegistryCommand.swift @@ -24,7 +24,6 @@ extension Application { subcommands: [ Login.self, Logout.self, - RegistryDefault.self, ], aliases: ["r"] ) diff --git a/Sources/CLI/Registry/RegistryDefault.swift b/Sources/CLI/Registry/RegistryDefault.swift deleted file mode 100644 index 43457221..00000000 --- a/Sources/CLI/Registry/RegistryDefault.swift +++ /dev/null @@ -1,99 +0,0 @@ -//===----------------------------------------------------------------------===// -// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. -// -// 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 ArgumentParser -import ContainerClient -import ContainerPersistence -import ContainerizationError -import ContainerizationOCI -import Foundation - -extension Application { - struct RegistryDefault: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "default", - abstract: "Manage the default image registry", - subcommands: [ - DefaultSetCommand.self, - DefaultUnsetCommand.self, - DefaultInspectCommand.self, - ] - ) - } - - struct DefaultSetCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "set", - abstract: "Set the default registry" - ) - - @OptionGroup - var global: Flags.Global - - @OptionGroup - var registry: Flags.Registry - - @Argument - var host: String - - func run() async throws { - let scheme = try RequestScheme(registry.scheme).schemeFor(host: host) - - let _url = "\(scheme)://\(host)" - guard let url = URL(string: _url), let domain = url.host() else { - throw ContainerizationError(.invalidArgument, message: "Cannot convert \(_url) to URL") - } - let resolvedDomain = Reference.resolveDomain(domain: domain) - let client = RegistryClient(host: resolvedDomain, scheme: scheme.rawValue, port: url.port) - do { - try await client.ping() - } catch let err as RegistryClient.Error { - switch err { - case .invalidStatus(url: _, .unauthorized, _), .invalidStatus(url: _, .forbidden, _): - break - default: - throw err - } - } - DefaultsStore.set(value: host, key: .defaultRegistryDomain) - print("Set default registry to \(host)") - } - } - - struct DefaultUnsetCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "unset", - abstract: "Unset the default registry", - aliases: ["clear"] - ) - - func run() async throws { - DefaultsStore.unset(key: .defaultRegistryDomain) - print("Unset the default registry domain") - } - } - - struct DefaultInspectCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "inspect", - abstract: "Display the default registry domain" - ) - - func run() async throws { - print(DefaultsStore.get(key: .defaultRegistryDomain)) - } - } -} diff --git a/Sources/CLI/System/DNS/DNSDefault.swift b/Sources/CLI/System/DNS/DNSDefault.swift deleted file mode 100644 index b3f07448..00000000 --- a/Sources/CLI/System/DNS/DNSDefault.swift +++ /dev/null @@ -1,72 +0,0 @@ -//===----------------------------------------------------------------------===// -// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. -// -// 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 ArgumentParser -import ContainerPersistence - -extension Application { - struct DNSDefault: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "default", - abstract: "Set or unset the default local DNS domain", - subcommands: [ - DefaultSetCommand.self, - DefaultUnsetCommand.self, - DefaultInspectCommand.self, - ] - ) - - struct DefaultSetCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "set", - abstract: "Set the default local DNS domain" - - ) - - @Argument(help: "the default `--domain-name` to use for the `create` or `run` command") - var domainName: String - - func run() async throws { - DefaultsStore.set(value: domainName, key: .defaultDNSDomain) - print(domainName) - } - } - - struct DefaultUnsetCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "unset", - abstract: "Unset the default local DNS domain", - aliases: ["clear"] - ) - - func run() async throws { - DefaultsStore.unset(key: .defaultDNSDomain) - print("Unset the default local DNS domain") - } - } - - struct DefaultInspectCommand: AsyncParsableCommand { - static let configuration = CommandConfiguration( - commandName: "inspect", - abstract: "Display the default local DNS domain" - ) - - func run() async throws { - print(DefaultsStore.getOptional(key: .defaultDNSDomain) ?? "") - } - } - } -} diff --git a/Sources/CLI/System/Property/PropertyClear.swift b/Sources/CLI/System/Property/PropertyClear.swift new file mode 100644 index 00000000..dff042f3 --- /dev/null +++ b/Sources/CLI/System/Property/PropertyClear.swift @@ -0,0 +1,44 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// 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 ArgumentParser +import ContainerClient +import ContainerPersistence +import ContainerizationError +import Foundation + +extension Application { + struct PropertyClear: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "clear", + abstract: "Clear a property value" + ) + + @OptionGroup + var global: Flags.Global + + @Argument(help: "the property ID") + var id: String + + func run() async throws { + guard let key = DefaultsStore.Keys(rawValue: id) else { + throw ContainerizationError(.invalidArgument, message: "invalid property ID: \(id)") + } + + DefaultsStore.unset(key: key) + } + } +} diff --git a/Sources/CLI/System/Property/PropertyGet.swift b/Sources/CLI/System/Property/PropertyGet.swift new file mode 100644 index 00000000..a5bf7c15 --- /dev/null +++ b/Sources/CLI/System/Property/PropertyGet.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// 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 ArgumentParser +import ContainerClient +import ContainerPersistence +import ContainerizationError +import Foundation + +extension Application { + struct PropertyGet: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "get", + abstract: "Retrieve a property value" + ) + + @OptionGroup + var global: Flags.Global + + @Argument(help: "the property ID") + var id: String + + func run() async throws { + let value = DefaultsStore.allValues() + .filter { id == $0.id } + .first + guard let value else { + throw ContainerizationError(.invalidArgument, message: "property ID \(id) not found") + } + + guard let val = value.value?.description else { + return + } + + print(val) + } + } +} diff --git a/Sources/CLI/System/Property/PropertyList.swift b/Sources/CLI/System/Property/PropertyList.swift new file mode 100644 index 00000000..2d834c6c --- /dev/null +++ b/Sources/CLI/System/Property/PropertyList.swift @@ -0,0 +1,93 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// 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 ArgumentParser +import ContainerClient +import ContainerPersistence +import Foundation + +extension Application { + struct PropertyList: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "list", + abstract: "List system properties", + aliases: ["ls"] + ) + + @Flag(name: .shortAndLong, help: "Only output the network name") + var quiet = false + + @Option(name: .long, help: "Format of the output") + var format: ListFormat = .table + + @OptionGroup + var global: Flags.Global + + func run() async throws { + let vals = DefaultsStore.allValues() + try printValues(vals, format: format) + } + + private func createHeader() -> [[String]] { + [["ID", "TYPE", "VALUE", "DESCRIPTION"]] + } + + private func printValues(_ vals: [DefaultsStoreValue], format: ListFormat) throws { + if format == .json { + let data = try JSONEncoder().encode(vals) + print(String(data: data, encoding: .utf8)!) + return + } + + if self.quiet { + vals.forEach { + print($0.id) + } + return + } + + var rows = createHeader() + for property in vals { + rows.append(property.asRow) + } + + let formatter = TableOutput(rows: rows) + print(formatter.format()) + } + } +} + +extension DefaultsStoreValue { + var asRow: [String] { + [id, String(describing: type), value?.description.elided(to: 40) ?? "*undefined*", description] + } +} + +extension String { + func elided(to maxCount: Int) -> String { + let ellipsis = "..." + guard self.count > maxCount else { + return self + } + + if maxCount < ellipsis.count { + return ellipsis + } + + let prefixCount = maxCount - ellipsis.count + return self.prefix(prefixCount) + ellipsis + } +} diff --git a/Sources/CLI/System/Property/PropertySet.swift b/Sources/CLI/System/Property/PropertySet.swift new file mode 100644 index 00000000..4c3deefa --- /dev/null +++ b/Sources/CLI/System/Property/PropertySet.swift @@ -0,0 +1,78 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// 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 ArgumentParser +import ContainerClient +import ContainerPersistence +import ContainerizationError +import ContainerizationExtras +import ContainerizationOCI +import Foundation + +extension Application { + struct PropertySet: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "set", + abstract: "Set a property value" + ) + + @OptionGroup + var global: Flags.Global + + @Argument(help: "the property ID") + var id: String + + @Argument(help: "the property value") + var value: String + + func run() async throws { + guard let key = DefaultsStore.Keys(rawValue: id) else { + throw ContainerizationError(.invalidArgument, message: "invalid property ID: \(id)") + } + + switch key { + case .buildRosetta: + guard let boolValue = Parser.parseBool(string: value) else { + throw ContainerizationError(.invalidArgument, message: "invalid boolean value: \(value)") + } + DefaultsStore.setBool(value: boolValue, key: key) + case .defaultDNSDomain, .defaultRegistryDomain: + guard Parser.isValidDomainName(value) else { + throw ContainerizationError(.invalidArgument, message: "invalid domain name: \(value)") + } + DefaultsStore.set(value: value, key: key) + case .defaultBuilderImage, .defaultInitImage: + guard (try? Reference.parse(value)) != nil else { + throw ContainerizationError(.invalidArgument, message: "invalid image reference: \(value)") + } + DefaultsStore.set(value: value, key: key) + case .defaultKernelBinaryPath: + DefaultsStore.set(value: value, key: key) + case .defaultKernelURL: + guard URL(string: value) != nil else { + throw ContainerizationError(.invalidArgument, message: "invalid URL: \(value)") + } + DefaultsStore.set(value: value, key: key) + return + case .defaultSubnet: + guard (try? CIDRAddress(value)) != nil else { + throw ContainerizationError(.invalidArgument, message: "invalid CIDRv4 address: \(value)") + } + DefaultsStore.set(value: value, key: key) + } + } + } +} diff --git a/Sources/CLI/System/SystemCommand.swift b/Sources/CLI/System/SystemCommand.swift index 3a92bfb9..2814baf0 100644 --- a/Sources/CLI/System/SystemCommand.swift +++ b/Sources/CLI/System/SystemCommand.swift @@ -23,11 +23,12 @@ extension Application { abstract: "Manage system components", subcommands: [ SystemDNS.self, - SystemLogs.self, - SystemStart.self, - SystemStop.self, - SystemStatus.self, SystemKernel.self, + SystemLogs.self, + SystemProperty.self, + SystemStart.self, + SystemStatus.self, + SystemStop.self, ], aliases: ["s"] ) diff --git a/Sources/CLI/System/SystemDNS.swift b/Sources/CLI/System/SystemDNS.swift index 4f9b3e3b..6c3840cf 100644 --- a/Sources/CLI/System/SystemDNS.swift +++ b/Sources/CLI/System/SystemDNS.swift @@ -27,7 +27,6 @@ extension Application { DNSCreate.self, DNSDelete.self, DNSList.self, - DNSDefault.self, ] ) } diff --git a/Sources/CLI/System/SystemProperty.swift b/Sources/CLI/System/SystemProperty.swift new file mode 100644 index 00000000..4404f565 --- /dev/null +++ b/Sources/CLI/System/SystemProperty.swift @@ -0,0 +1,35 @@ +//===----------------------------------------------------------------------===// +// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved. +// +// 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 ArgumentParser +import ContainerPersistence +import ContainerizationError +import Foundation + +extension Application { + struct SystemProperty: AsyncParsableCommand { + static let configuration = CommandConfiguration( + commandName: "property", + abstract: "Manage system property values", + subcommands: [ + PropertyClear.self, + PropertyGet.self, + PropertyList.self, + PropertySet.self, + ] + ) + } +} diff --git a/Sources/ContainerClient/Parser.swift b/Sources/ContainerClient/Parser.swift index 8ae312d0..493bf9fb 100644 --- a/Sources/ContainerClient/Parser.swift +++ b/Sources/ContainerClient/Parser.swift @@ -640,4 +640,32 @@ public struct Parser { "invalid publish-socket format \(socketText). Expected: host_path:container_path") } } + + // MARK: DNS + + public static func isValidDomainName(_ name: String) -> Bool { + guard !name.isEmpty && name.count <= 255 else { + return false + } + return name.components(separatedBy: ".").allSatisfy { Self.isValidDomainNameLabel($0) } + } + + public static func isValidDomainNameLabel(_ label: String) -> Bool { + guard !label.isEmpty && label.count <= 63 else { + return false + } + let pattern = #/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/# + return !label.ranges(of: pattern).isEmpty + } + + // MARK: Miscellaneous + + public static func parseBool(string: String) -> Bool? { + let lower = string.lowercased() + switch lower { + case "true", "t": return true + case "false", "f": return false + default: return nil + } + } } diff --git a/Sources/ContainerPersistence/DefaultsStore.swift b/Sources/ContainerPersistence/DefaultsStore.swift index 54ac2f6b..54f5d5a5 100644 --- a/Sources/ContainerPersistence/DefaultsStore.swift +++ b/Sources/ContainerPersistence/DefaultsStore.swift @@ -22,14 +22,14 @@ public enum DefaultsStore { private static let userDefaultDomain = "com.apple.container.defaults" public enum Keys: String { - case defaultBuilderImage = "image.builder" - case defaultDNSDomain = "dns.domain" - case defaultRegistryDomain = "registry.domain" - case defaultInitImage = "image.init" - case defaultKernelURL = "kernel.url" - case defaultKernelBinaryPath = "kernel.binaryPath" - case defaultSubnet = "network.subnet" case buildRosetta = "build.rosetta" + case defaultDNSDomain = "dns.domain" + case defaultBuilderImage = "image.builder" + case defaultInitImage = "image.init" + case defaultKernelBinaryPath = "kernel.binaryPath" + case defaultKernelURL = "kernel.url" + case defaultSubnet = "network.subnet" + case defaultRegistryDomain = "registry.domain" } public static func set(value: String, key: DefaultsStore.Keys) { @@ -58,6 +58,23 @@ public enum DefaultsStore { return udSuite.bool(forKey: key.rawValue) } + public static func allValues() -> [DefaultsStoreValue] { + let allKeys: [(Self.Keys, (Self.Keys) -> Any?)] = [ + (.buildRosetta, { Self.getBool(key: $0) }), + (.defaultBuilderImage, { Self.get(key: $0) }), + (.defaultInitImage, { Self.get(key: $0) }), + (.defaultKernelBinaryPath, { Self.get(key: $0) }), + (.defaultKernelURL, { Self.get(key: $0) }), + (.defaultSubnet, { Self.getOptional(key: $0) }), + (.defaultDNSDomain, { Self.getOptional(key: $0) }), + (.defaultRegistryDomain, { Self.get(key: $0) }), + ] + return + allKeys + .map { DefaultsStoreValue(id: $0.rawValue, description: $0.summary, value: $1($0) as? (Encodable & CustomStringConvertible), type: $0.type) } + .sorted(by: { $0.id < $1.id }) + } + private static var udSuite: UserDefaults { guard let ud = UserDefaults.init(suiteName: self.userDefaultDomain) else { fatalError("Failed to initialize UserDefaults for domain \(self.userDefaultDomain)") @@ -66,31 +83,98 @@ public enum DefaultsStore { } } +public struct DefaultsStoreValue: Identifiable, CustomStringConvertible, Encodable { + public let id: String + public let description: String + public let value: (Encodable & CustomStringConvertible)? + public let type: Any.Type + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(description, forKey: .description) + + if let value = value { + try container.encode(value, forKey: .value) + } else { + try container.encodeNil(forKey: .value) + } + + try container.encode(String(describing: type), forKey: .type) + } + + enum CodingKeys: String, CodingKey { + case id, description, value, type + } +} + extension DefaultsStore.Keys { + public var summary: String { + switch self { + case .buildRosetta: + return "Build amd64 images on arm64 using Rosetta, instead of QEMU." + case .defaultDNSDomain: + return "If defined, the local DNS domain to use for containers with unqualified names." + case .defaultBuilderImage: + return "The image reference for the utility container that `container build` uses." + case .defaultInitImage: + return "The image reference for the default initial filesystem image." + case .defaultKernelBinaryPath: + return "If the kernel URL is for an archive, the archive member pathname for the kernel file." + case .defaultKernelURL: + return "The URL for the kernel file to install, or the URL for an archive containing the kernel file." + case .defaultSubnet: + return "Default subnet for IP allocation (used on macOS 15 only)." + case .defaultRegistryDomain: + return "The default registry to use for image references that do not specify a registry." + } + } + + public var type: Any.Type { + switch self { + case .buildRosetta: + return Bool.self + case .defaultDNSDomain: + return String.self + case .defaultBuilderImage: + return String.self + case .defaultInitImage: + return String.self + case .defaultKernelBinaryPath: + return String.self + case .defaultKernelURL: + return String.self + case .defaultSubnet: + return String.self + case .defaultRegistryDomain: + return String.self + } + } + fileprivate var defaultValue: String { switch self { - case .defaultKernelURL: - return "https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz" - case .defaultKernelBinaryPath: - return "opt/kata/share/kata-containers/vmlinux-6.12.28-153" + case .buildRosetta: + // This is a boolean key, not used with the string get() method + return "true" + case .defaultDNSDomain: + return "test" case .defaultBuilderImage: let tag = String(cString: get_container_builder_shim_version()) return "ghcr.io/apple/container-builder-shim/builder:\(tag)" - case .defaultDNSDomain: - return "test" - case .defaultRegistryDomain: - return "docker.io" case .defaultInitImage: let tag = String(cString: get_swift_containerization_version()) guard tag != "latest" else { return "vminit:latest" } return "ghcr.io/apple/containerization/vminit:\(tag)" + case .defaultKernelBinaryPath: + return "opt/kata/share/kata-containers/vmlinux-6.12.28-153" + case .defaultKernelURL: + return "https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz" case .defaultSubnet: return "192.168.64.1/24" - case .buildRosetta: - // This is a boolean key, not used with the string get() method - return "true" + case .defaultRegistryDomain: + return "docker.io" } } } diff --git a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift index 44a5e7bd..6420276f 100644 --- a/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift +++ b/Tests/CLITests/Subcommands/Run/TestCLIRunOptions.swift @@ -510,7 +510,7 @@ class TestCLIRunCommand: CLITest { } func getDefaultDomain() throws -> String? { - let (output, err, status) = try run(arguments: ["system", "dns", "default", "inspect"]) + let (output, err, status) = try run(arguments: ["system", "property", "get", "dns.domain"]) try #require(status == 0, "default DNS domain retrieval returned status \(status): \(err)") let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedOutput == "" { diff --git a/Tests/CLITests/Utilities/CLITest.swift b/Tests/CLITests/Utilities/CLITest.swift index e7a6f339..96051ff9 100644 --- a/Tests/CLITests/Utilities/CLITest.swift +++ b/Tests/CLITests/Utilities/CLITest.swift @@ -406,9 +406,10 @@ class CLITest { func doDefaultRegistrySet(domain: String) throws { let args = [ - "registry", - "default", + "system", + "property", "set", + "registry.domain", domain, ] let (_, error, status) = try run(arguments: args) @@ -419,9 +420,10 @@ class CLITest { func doDefaultRegistryUnset() throws { let args = [ - "registry", - "default", - "unset", + "system", + "property", + "clear", + "registry.domain", ] let (_, error, status) = try run(arguments: args) if status != 0 { diff --git a/Tests/ContainerClientTests/ParserTest.swift b/Tests/ContainerClientTests/ParserTest.swift index f076a7da..f561569b 100644 --- a/Tests/ContainerClientTests/ParserTest.swift +++ b/Tests/ContainerClientTests/ParserTest.swift @@ -225,4 +225,45 @@ struct ParserTest { return error.description.contains("path") && error.description.contains("is not a directory") } } + + @Test + func testIsValidDomainNameOk() throws { + let names = [ + "a", + "a.b", + "foo.bar", + "F-O.B-R", + [ + String(repeating: "0", count: 63), + String(repeating: "1", count: 63), + String(repeating: "2", count: 63), + String(repeating: "3", count: 63), + ].joined(separator: "."), + ] + for name in names { + #expect(Parser.isValidDomainName(name)) + } + } + + @Test + func testIsValidDomainNameBad() throws { + let names = [ + ".foo", + "foo.", + ".foo.bar", + "foo.bar.", + "-foo.bar", + "foo.bar-", + [ + String(repeating: "0", count: 63), + String(repeating: "1", count: 63), + String(repeating: "2", count: 63), + String(repeating: "3", count: 62), + "4", + ].joined(separator: "."), + ] + for name in names { + #expect(!Parser.isValidDomainName(name)) + } + } } diff --git a/docs/command-reference.md b/docs/command-reference.md index 9012f948..751558d5 100644 --- a/docs/command-reference.md +++ b/docs/command-reference.md @@ -610,16 +610,6 @@ container registry logout SERVER Only `--version` and `-h`/`--help` are available. -### `container registry default` commands - -The `registry default` group allows setting, unsetting, and inspecting the default registry used when no registry is specified on image references. - -* `container registry default set [OPTIONS] HOST`: Set the default registry. - * `--scheme `: registry scheme. One of (`http`, `https`, `auto`) (default: `auto`) - * **Global**: `--debug`, `--version`, `-h`/`--help` -* `container registry default unset (clear)`: Clears the default registry configuration. -* `container registry default inspect`: Displays the current default registry, if any. - ## System Management System commands manage the container apiserver, logs, DNS settings and kernel. These are only available on macOS hosts. @@ -725,14 +715,6 @@ container system dns list No options. -### `container system dns default` commands - -Manage the default local DNS domain used by other commands. - -* `container system dns default set NAME`: Set the default DNS domain used by `create`/`run`. -* `container system dns default unset (clear)`: Unset the default DNS domain. -* `container system dns default inspect`: Display the current default DNS domain. - ### `container system kernel set` Installs or updates the Linux kernel used by the container runtime on macOS hosts. @@ -751,4 +733,124 @@ container system kernel set [OPTIONS] * `--recommended`: Download and install the recommended default kernel for your host * **Global**: `--debug`, `--version`, `-h`/`--help` -*** +### `container system property list (ls)` + +Lists all available system properties with their current values, types, and descriptions. Output can be formatted as a table or JSON. + +**Usage** + +```bash +container system property list [OPTIONS] +``` + +**Options** + +* `-q, --quiet`: Only output the property IDs +* `--format `: Format of the output (values: `json`, `table`; default: `table`) +* **Global**: `--debug`, `--version`, `-h`/`--help` + +**Examples** + +```bash +# list all properties in table format +container system property list + +# get only property IDs +container system property list --quiet + +# output as JSON for scripting +container system property list --format json +``` + +### `container system property get` + +Retrieves the current value of a specific system property by its ID. + +**Usage** + +```bash +container system property get PROPERTY_ID +``` + +**Arguments** + +* `PROPERTY_ID`: The ID of the property to retrieve (use `property list` to see available IDs) + +**Global flags**: `--debug`, `--version`, `-h`/`--help` + +**Examples** + +```bash +# get the default registry domain +container system property get registry.domain + +# get the current DNS domain setting +container system property get dns.domain +``` + +### `container system property set` + +Sets the value of a system property. The command validates the value based on the property type (boolean, domain name, image reference, URL, or CIDR address). + +**Usage** + +```bash +container system property set PROPERTY_ID VALUE +``` + +**Arguments** + +* `PROPERTY_ID`: The ID of the property to set +* `VALUE`: The new value for the property + +**Property Types and Validation** + +* **Boolean properties** (`build.rosetta`): Accepts `true`, `t`, `false`, `f` (case-insensitive) +* **Domain properties** (`dns.domain`, `registry.domain`): Must be valid domain names +* **Image properties** (`image.builder`, `image.init`): Must be valid OCI image references +* **URL properties** (`kernel.url`): Must be valid URLs +* **Network properties** (`network.subnet`): Must be valid CIDR addresses +* **Path properties** (`kernel.binaryPath`): Accept any string value + +**Global flags**: `--debug`, `--version`, `-h`/`--help` + +**Examples** + +```bash +# enable Rosetta for AMD64 builds on ARM64 +container system property set build.rosetta true + +# set a custom DNS domain +container system property set dns.domain mycompany.local + +# configure a custom registry +container system property set registry.domain registry.example.com + +# set a custom builder image +container system property set image.builder myregistry.com/custom-builder:latest +``` + +### `container system property clear` + +Clears (unsets) a system property, reverting it to its default value. + +**Usage** + +```bash +container system property clear PROPERTY_ID +``` + +**Arguments** + +* `PROPERTY_ID`: The ID of the property to clear + +**Global flags**: `--debug`, `--version`, `-h`/`--help` + +**Examples** + +```bash +# clear custom DNS domain (revert to default) +container system property clear dns.domain + +# clear custom registry setting +container system property clear registry.domain diff --git a/docs/how-to.md b/docs/how-to.md index 6d384d0e..7d55c965 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -358,18 +358,30 @@ container run --name nested-virtualization --virtualization --kernel /path/to/a/ [ 0.017893] kvm [1]: Hyp mode initialized successfully ``` -## Configure container defaults +## Configure system properties -`container` uses macOS user defaults to store configuration settings that persist between sessions. You can customize various aspects of container behavior, including build settings, default images, and network configuration. +The `container system property` subcommand manages the configuration settings for the `container` CLI and services. You can customize various aspects of container behavior, including build settings, default images, and network configuration. -For a complete list of available configuration options and detailed usage instructions, see the [user defaults documentation](user-defaults.md). +Use `container system property list` to show information for all available properties: + +```console +% bin/container system property ls +ID TYPE VALUE DESCRIPTION +build.rosetta Bool true Build amd64 images on arm64 using Rosetta, instead of QEMU. +dns.domain String *undefined* If defined, the local DNS domain to use for containers with unqualified names. +image.builder String ghcr.io/apple/container-builder-shim/... The image reference for the utility container that `container build` uses. +image.init String ghcr.io/apple/containerization/vminit... The image reference for the default initial filesystem image. +kernel.binaryPath String opt/kata/share/kata-containers/vmlinu... If the kernel URL is for an archive, the archive member pathname for the kernel file. +kernel.url String https://github.com/kata-containers/ka... The URL for the kernel file to install, or the URL for an archive containing the kernel file. +network.subnet String *undefined* Default subnet for IP allocation (used on macOS 15 only). +``` ### Example: Disable Rosetta for builds If you want to prevent the use of Rosetta translation during container builds on Apple Silicon Macs: ```bash -defaults write com.apple.container.defaults build.rosetta -bool false +container system property set build.rosetta false ``` This is useful when you want to ensure builds only produce native arm64 images and avoid any x86_64 emulation. diff --git a/docs/tutorial.md b/docs/tutorial.md index 9654ffcc..a7f3ee31 100644 --- a/docs/tutorial.md +++ b/docs/tutorial.md @@ -98,7 +98,7 @@ Use the `--help` flag to see which abbreviations exist. ```bash sudo container system dns create test -container system dns default set test +container system property set dns.domain test ``` Enter your administrator password when prompted. The first command requires administrator privileges to create a file containing the domain configuration under the `/etc/resolver` directory, and to tell the macOS DNS resolver to reload its configuration files. @@ -253,39 +253,39 @@ Push your image to a container registry, publishing it so that you and others ca ### Publish the web server image -To publish your image, you need push images to a registry service that stores the image for future use. Typically, you need to authenticate with a registry to push an image. This example assumes that you have an account at a hypothetical registry named `registry.example.com` with username `fido` and a password or token `my-secret`, and that your personal repository name is the same as your username. - -> [!NOTE] -> By default `container` is configured to use Docker Hub. -> You can change the default registry used by running `container registry default set `. -> See the other sub commands under `container registry` for more options. +To publish your image, you need push images to a registry service that stores the image for future use. Typically, you need to authenticate with a registry to push an image. This example assumes that you have an account at a hypothetical registry named `some-registry.example.com` with username `fido` and a password or token `my-secret`, and that your personal repository name is the same as your username. To sign into a secure registry with your login credentials, enter your username and password at the prompts after running: ```bash -container registry login {registry.example.com} +container registry login some-registry.example.com ``` Create another name for your image that includes the registry name, your repository name, and the image name, with the tag `latest`: ```bash -container image tag web-test {registry.example.com/fido}/web-test:latest +container image tag web-test some-registry.example.com/fido/web-test:latest ``` Then, push the image: ```bash -container image push {registry.example.com/fido}/web-test:latest +container image push some-registry.example.com/fido/web-test:latest ``` +> [!NOTE] +> By default `container` is configured to use Docker Hub. +> You can change the default registry to another value by running `container system property set registry.domain some-registry.example.com`. +> See the other sub commands under `container registry` for more options. + ### Pull and run your image To validate your published image, stop your current web server container, remove the image that you built, and then run using the remote image: ```bash container stop my-web-server -container image delete web-test {registry.example.com/fido}/web-test:latest -container run --name my-web-server --detach --rm {registry.example.com/fido}/web-test:latest +container image delete web-test some-registry.example.com/fido/web-test:latest +container run --name my-web-server --detach --rm some-registry.example.com/fido/web-test:latest ``` ## Clean up diff --git a/docs/user-defaults.md b/docs/user-defaults.md deleted file mode 100644 index f6aa547c..00000000 --- a/docs/user-defaults.md +++ /dev/null @@ -1,150 +0,0 @@ -# User Defaults Configuration - -The `container` CLI uses macOS user defaults to store configuration settings. These settings persist between sessions and allow you to customize the behavior of various commands. - -## Viewing Current Defaults - -To view a specific default value, use the macOS `defaults` command: - -```bash -defaults read com.apple.container.defaults -``` - -## Available User Defaults - -### Build Settings - -#### build.rosetta - -Controls whether Rosetta translation is enabled during container builds. When enabled (default), allows building x86_64 images on Apple Silicon Macs. - -**Default:** `true` - -**Usage:** -```bash -# Disable Rosetta for builds -defaults write com.apple.container.defaults build.rosetta -bool false - -# Enable Rosetta for builds (default) -defaults write com.apple.container.defaults build.rosetta -bool true - -# Check current value -defaults read com.apple.container.defaults build.rosetta -``` - -**Note:** Disabling Rosetta will prevent building x86_64 images on Apple Silicon Macs. This setting only affects the build process and does not impact running containers. - -### Image Settings - -#### image.builder - -Specifies the default BuildKit image used for building containers. - -**Default:** `ghcr.io/apple/container-builder-shim/builder:` - -**Usage:** -```bash -# Set a custom builder image -defaults write com.apple.container.defaults image.builder "my-registry.com/my-builder:latest" - -# Reset to default -defaults delete com.apple.container.defaults image.builder -``` - -#### image.init - -Specifies the default init image used for container initialization. - -**Default:** `ghcr.io/apple/containerization/vminit:` - -**Usage:** -```bash -# Set a custom init image -defaults write com.apple.container.defaults image.init "my-registry.com/my-init:latest" -``` - -### Network Settings - -#### dns.domain - -Sets the default local DNS domain for containers. - -**Default:** `test` - -**Usage:** -```bash -# Set a custom DNS domain -defaults write com.apple.container.defaults dns.domain "mycompany.local" - -# Alternatively, use the container CLI -container system dns default set mycompany.local -``` - -### Registry Settings - -#### registry.domain - -Sets the default registry domain for pulling images. - -**Default:** `docker.io` - -**Usage:** -```bash -# Set a custom default registry -defaults write com.apple.container.defaults registry.domain "ghcr.io" - -# Alternatively, use the container CLI -container registry default set ghcr.io -``` - -### Kernel Settings - -#### kernel.url - -URL for downloading the default kernel used by containers. - -**Default:** `https://github.com/kata-containers/kata-containers/releases/download/3.17.0/kata-static-3.17.0-arm64.tar.xz` - -**Usage:** -```bash -# Set a custom kernel URL -defaults write com.apple.container.defaults kernel.url "https://myserver.com/custom-kernel.tar.xz" -``` - -#### kernel.binaryPath - -Path within the kernel archive to the actual kernel binary. - -**Default:** `opt/kata/share/kata-containers/vmlinux-6.12.28-153` - -**Usage:** -```bash -# Set a custom kernel binary path -defaults write com.apple.container.defaults kernel.binaryPath "path/to/vmlinux" -``` - -## Resetting Defaults - -To reset a specific setting to its default value: - -```bash -defaults delete com.apple.container.defaults -``` - -To reset all container defaults: - -```bash -defaults delete com.apple.container.defaults -``` - -## Platform-Specific Settings - -### macOS 15 Network Configuration - -On macOS 15, if you experience network connectivity issues, you may need to manually configure the network subnet: - -```bash -defaults write com.apple.container.defaults network.subnet 192.168.66.1/24 -``` - -See the [technical overview](technical-overview.md#macos-15-limitations) for more details about macOS 15 limitations. \ No newline at end of file