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:
J Logan
2025-09-16 13:37:55 -07:00
committed by GitHub
parent 386fd87b5d
commit 449f1d23df
19 changed files with 634 additions and 386 deletions
@@ -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))
}
}
}
-72
View File
@@ -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)
}
}
}
}
+5 -4
View File
@@ -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"]
)
-1
View File
@@ -27,7 +27,6 @@ extension Application {
DNSCreate.self,
DNSDelete.self,
DNSList.self,
DNSDefault.self,
]
)
}
+35
View File
@@ -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,
]
)
}
}
+28
View File
@@ -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
}
}
}
+102 -18
View File
@@ -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 == "" {
+7 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
-150
View File
@@ -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.