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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 3 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ PKG_PATH := bin/$(BUILD_CONFIGURATION)/container-installer-unsigned.pkg
DSYM_DIR := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM
DSYM_PATH := bin/$(BUILD_CONFIGURATION)/bundle/container-dSYM.zip
CODESIGN_OPTS ?= --force --sign - --timestamp=none
INTEGRATION_TEST_FILTER ?= TestCLI

MACOS_VERSION := $(shell sw_vers -productVersion)
MACOS_MAJOR := $(shell echo $(MACOS_VERSION) | cut -d. -f1)
Expand Down Expand Up @@ -119,7 +120,7 @@ dsym:

.PHONY: test
test:
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --skip TestCLI
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --skip TestCLI $$(if [ -n "$(TEST_FILTER)" ]; then echo "--filter $(TEST_FILTER)"; fi)

.PHONY: install-kernel
install-kernel:
Expand All @@ -135,13 +136,7 @@ integration: init-block
@echo "Removing any existing containers"
@bin/container rm --all
@echo "Starting CLI integration tests"
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLINetwork
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunLifecycle
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIExecCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIImagesCommand
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIRunBase
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --filter TestCLIBuildBase
@$(SWIFT) test -c $(BUILD_CONFIGURATION) --no-parallel --filter $(INTEGRATION_TEST_FILTER) $$(if [ -n "$(INTEGRATION_TEST_SKIP)" ]; then echo "--skip $(INTEGRATION_TEST_SKIP)"; fi)
@echo Ensuring apiserver stopped after the CLI integration tests...
@scripts/ensure-container-stopped.sh

Expand Down
7 changes: 7 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,11 @@ let package = Package(
name: "ContainerClientTests",
dependencies: [
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "Logging", package: "swift-log"),
"ContainerClient",
"ContainerSandboxService",
"ContainerXPC",
]
),
.target(
Expand Down Expand Up @@ -290,9 +294,12 @@ let package = Package(
.product(name: "Containerization", package: "containerization"),
.product(name: "ContainerizationExtras", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "ContainerizationOCI", package: "containerization"),
"ContainerSandboxService",
"ContainerBuild",
"ContainerClient",
"ContainerNetworkService",
"ContainerXPC",
],
path: "Tests/CLITests"
),
Expand Down
11 changes: 8 additions & 3 deletions Sources/CLI/Application.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ struct Application: AsyncParsableCommand {
CommandGroup(
name: "Container",
subcommands: [
ContainerAttach.self,
ContainerCreate.self,
ContainerDelete.self,
ContainerExec.self,
Expand Down Expand Up @@ -148,7 +149,7 @@ struct Application: AsyncParsableCommand {
}
}

static func handleProcess(io: ProcessIO, process: ClientProcess) async throws -> Int32 {
static func handleProcess(io: ProcessIO, process: ClientProcess, serverOwnedStdio: Bool = false) async throws -> Int32 {
let signals = AsyncSignalHandler.create(notify: Application.signalSet)
return try await withThrowingTaskGroup(of: Int32?.self, returning: Int32.self) { group in
let waitAdded = group.addTaskUnlessCancelled {
Expand All @@ -162,11 +163,15 @@ struct Application: AsyncParsableCommand {
return -1
}

try await process.start(io.stdio)
if !serverOwnedStdio {
try await process.start(io.stdio)
}
defer {
try? io.close()
}
try io.closeAfterStart()
if !serverOwnedStdio {
try io.closeAfterStart()
}

if let current = io.console {
let size = try current.size
Expand Down
82 changes: 82 additions & 0 deletions Sources/CLI/Container/ContainerAttach.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//===----------------------------------------------------------------------===//
// 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 ContainerizationError
import ContainerizationOS
import Foundation

extension Application {
struct ContainerAttach: AsyncParsableCommand {
static let configuration = CommandConfiguration(
commandName: "attach",
abstract: "Attach to a running container with PTY session support")

@OptionGroup
var global: Flags.Global

@Flag(name: .long, help: "Do not send buffered history when attaching")
var noHistory = false

@Option(name: .customLong("detach-keys"), help: "Override the key sequence for detaching a container")
var detachKeys: String?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not be happier about this 😆 I was honestly hoping someone would implement it.


@Argument(help: "Container ID or name to attach to")
var container: String

func run() async throws {
// Get container info
let container = try await ClientContainer.get(id: container)
try ensureRunning(container: container)

// Check if container has terminal enabled
guard container.configuration.initProcess.terminal else {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why? If you just wanted to attach to see stdout/err really quick this shouldn't be required I'd imagine. Likewise even if you wanted to pipe some stdin I'd think you wouldn't need a pty

throw ContainerizationError(
.invalidArgument,
message: "Cannot attach to container without terminal enabled"
)
}

log.info("Attaching to container", metadata: ["container": "\(container.id)"])

// Create attach client process
let process = try await container.attachToSession(sendHistory: !noHistory)

// Run the attached session
let exitCode = try await runAttachedSession(process: process)

if exitCode != 0 {
throw ArgumentParser.ExitCode(exitCode)
}
}

private func runAttachedSession(process: ClientProcess) async throws -> Int32 {
// Get handles from the attach response
let handles = try await process.getAttachHandles()

// Create ProcessIO for the attached session with detach key support
let io = try ProcessIO.createServerOwned(tty: true, handles: handles, detachKeys: detachKeys)

// Run with standard process handling
return try await Application.handleProcess(
io: io,
process: process,
serverOwnedStdio: true
)
}
}
}
Loading