mirror of
https://github.com/apple/container.git
synced 2026-07-12 12:37:02 +00:00
Replace scattered defaults subcommands with system property. (#604)
Common subcommands for all defaults. - Closes #384. - Replaces `registry default` and `system dns default` subcommands with `system property`. - Users can use `system property ls` to see details about each supported default value. - `system property set` implements reasonable validation for all properties. - NOTE: Probing of the registry for `registry default set` was removed, which means users will find out about a botched setting when pulling or pushing. - Updates docs. ## Type of Change - [ ] Bug fix - [x] New feature - [x] Breaking change - [x] Documentation update ## Motivation and Context See #384. ## Testing - [x] Tested locally - [x] Added/updated tests - [x] Added/updated docs
This commit is contained in:
@@ -24,7 +24,6 @@ extension Application {
|
||||
subcommands: [
|
||||
Login.self,
|
||||
Logout.self,
|
||||
RegistryDefault.self,
|
||||
],
|
||||
aliases: ["r"]
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
)
|
||||
|
||||
@@ -27,7 +27,6 @@ extension Application {
|
||||
DNSCreate.self,
|
||||
DNSDelete.self,
|
||||
DNSList.self,
|
||||
DNSDefault.self,
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+121
-19
@@ -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 <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>`: 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
|
||||
|
||||
+16
-4
@@ -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.
|
||||
|
||||
+12
-12
@@ -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 <registry url>`.
|
||||
> 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
|
||||
|
||||
@@ -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 <key>
|
||||
```
|
||||
|
||||
## 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:<version>`
|
||||
|
||||
**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:<version>`
|
||||
|
||||
**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 <key>
|
||||
```
|
||||
|
||||
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.
|
||||
Reference in New Issue
Block a user