mirror of
https://github.com/apple/container.git
synced 2026-08-24 10:05:43 -05:00
cli: Add support for --stop-signal (#1462)
- Closes #1581. - This adds stop signal support to the cli. The priority is: 1. If an explicit stop signal is passed on the cli use this. 2. If not, check if there is a stop signal in the image config. 3. Finally, use the default (TERM).
This commit is contained in:
@@ -19,7 +19,6 @@ import ContainerAPIClient
|
||||
import ContainerResource
|
||||
import Containerization
|
||||
import ContainerizationError
|
||||
import ContainerizationOS
|
||||
import Foundation
|
||||
import Logging
|
||||
|
||||
@@ -35,7 +34,7 @@ extension Application {
|
||||
var all = false
|
||||
|
||||
@Option(name: .shortAndLong, help: "Signal to send to the containers")
|
||||
var signal: String = "SIGTERM"
|
||||
var signal: String?
|
||||
|
||||
@Option(name: .shortAndLong, help: "Seconds to wait before killing the containers")
|
||||
var time: Int32 = 5
|
||||
@@ -68,7 +67,7 @@ extension Application {
|
||||
|
||||
let opts = ContainerStopOptions(
|
||||
timeoutInSeconds: self.time,
|
||||
signal: try Signal(self.signal).rawValue
|
||||
signal: self.signal
|
||||
)
|
||||
try await Self.stopContainers(
|
||||
client: client,
|
||||
|
||||
@@ -66,8 +66,7 @@ extension Application {
|
||||
log.info("stopping containers", metadata: ["stopTimeoutSeconds": "\(Self.stopTimeoutSeconds)"])
|
||||
do {
|
||||
let containers = try await client.list().map { $0.id }
|
||||
let signal = Signal.term.rawValue
|
||||
let opts = ContainerStopOptions(timeoutInSeconds: Self.stopTimeoutSeconds, signal: signal)
|
||||
let opts = ContainerStopOptions(timeoutInSeconds: Self.stopTimeoutSeconds, signal: nil)
|
||||
try await ContainerStop.stopContainers(
|
||||
client: client,
|
||||
containers: containers,
|
||||
|
||||
@@ -59,6 +59,8 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
public var capDrop: [String] = []
|
||||
/// Size of /dev/shm in bytes. When nil, the default size is used.
|
||||
public var shmSize: UInt64?
|
||||
/// Signal to send to the container process on stop (from image config).
|
||||
public var stopSignal: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
@@ -82,6 +84,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
case capAdd
|
||||
case capDrop
|
||||
case shmSize
|
||||
case stopSignal
|
||||
}
|
||||
|
||||
/// Create a configuration from the supplied Decoder, initializing missing
|
||||
@@ -116,6 +119,7 @@ public struct ContainerConfiguration: Sendable, Codable {
|
||||
capAdd = try container.decodeIfPresent([String].self, forKey: .capAdd) ?? []
|
||||
capDrop = try container.decodeIfPresent([String].self, forKey: .capDrop) ?? []
|
||||
shmSize = try container.decodeIfPresent(UInt64.self, forKey: .shmSize)
|
||||
stopSignal = try container.decodeIfPresent(String.self, forKey: .stopSignal)
|
||||
}
|
||||
|
||||
public struct DNSConfiguration: Sendable, Codable {
|
||||
|
||||
@@ -17,15 +17,15 @@
|
||||
import Foundation
|
||||
|
||||
public struct ContainerStopOptions: Sendable, Codable {
|
||||
public let timeoutInSeconds: Int32
|
||||
public let signal: Int32
|
||||
public var timeoutInSeconds: Int32
|
||||
public var signal: String?
|
||||
|
||||
public static let `default` = ContainerStopOptions(
|
||||
timeoutInSeconds: 5,
|
||||
signal: SIGTERM
|
||||
signal: nil
|
||||
)
|
||||
|
||||
public init(timeoutInSeconds: Int32, signal: Int32) {
|
||||
public init(timeoutInSeconds: Int32, signal: String?) {
|
||||
self.timeoutInSeconds = timeoutInSeconds
|
||||
self.signal = signal
|
||||
}
|
||||
|
||||
@@ -262,6 +262,7 @@ public struct Utility {
|
||||
let caps = try Parser.capabilities(capAdd: management.capAdd, capDrop: management.capDrop)
|
||||
config.capAdd = caps.capAdd
|
||||
config.capDrop = caps.capDrop
|
||||
config.stopSignal = imageConfig?.stopSignal
|
||||
|
||||
if let runtime = management.runtime {
|
||||
config.runtimeHandler = runtime
|
||||
|
||||
@@ -636,8 +636,13 @@ public actor ContainersService {
|
||||
return
|
||||
}
|
||||
|
||||
var resolvedOptions = options
|
||||
if resolvedOptions.signal == nil, let stopSignal = state.snapshot.configuration.stopSignal {
|
||||
resolvedOptions.signal = stopSignal
|
||||
}
|
||||
|
||||
do {
|
||||
try await client.stop(options: options)
|
||||
try await client.stop(options: resolvedOptions)
|
||||
} catch let err as ContainerizationError {
|
||||
if err.code != .interrupted {
|
||||
throw err
|
||||
@@ -842,7 +847,7 @@ public actor ContainersService {
|
||||
}
|
||||
let opts = ContainerStopOptions(
|
||||
timeoutInSeconds: 5,
|
||||
signal: SIGKILL
|
||||
signal: "SIGKILL"
|
||||
)
|
||||
let client = try state.getClient()
|
||||
try await client.stop(options: opts)
|
||||
|
||||
@@ -1204,7 +1204,8 @@ public actor RuntimeService {
|
||||
try await lc.wait()
|
||||
}
|
||||
group.addTask {
|
||||
try await lc.kill(Signal(rawValue: stopOpts.signal))
|
||||
let signal = try Signal(stopOpts.signal ?? "SIGTERM")
|
||||
try await lc.kill(signal)
|
||||
try await Task.sleep(for: .seconds(stopOpts.timeoutInSeconds))
|
||||
try await lc.kill(.kill)
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
//===----------------------------------------------------------------------===//
|
||||
// Copyright © 2026 Apple Inc. and the container project authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// https://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//===----------------------------------------------------------------------===//
|
||||
|
||||
import Foundation
|
||||
import Testing
|
||||
|
||||
class TestCLIStop: CLITest {
|
||||
private func getTestName() -> String {
|
||||
Test.current!.name.trimmingCharacters(in: ["(", ")"]).lowercased()
|
||||
}
|
||||
|
||||
@Test func testStopWithExplicitSignal() throws {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name)
|
||||
defer { try? doStop(name: name) }
|
||||
|
||||
try waitForContainerRunning(name)
|
||||
|
||||
try doStop(name: name, signal: "SIGTERM")
|
||||
let status = try getContainerStatus(name)
|
||||
#expect(status == "stopped")
|
||||
}
|
||||
|
||||
@Test func testStopWithoutSignal() throws {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name)
|
||||
defer { try? doStop(name: name) }
|
||||
|
||||
try waitForContainerRunning(name)
|
||||
|
||||
try doStop(name: name, signal: nil)
|
||||
let status = try getContainerStatus(name)
|
||||
#expect(status == "stopped")
|
||||
}
|
||||
|
||||
@Test func testStopSignalInInspect() throws {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name)
|
||||
defer { try? doStop(name: name) }
|
||||
|
||||
try waitForContainerRunning(name)
|
||||
|
||||
let inspect = try inspectContainer(name)
|
||||
// Alpine doesn't set a STOPSIGNAL, so this should be nil.
|
||||
#expect(inspect.configuration.stopSignal == nil)
|
||||
}
|
||||
|
||||
@Test func testStopIdempotent() throws {
|
||||
let name = getTestName()
|
||||
try doLongRun(name: name)
|
||||
defer { try? doStop(name: name) }
|
||||
|
||||
try waitForContainerRunning(name)
|
||||
|
||||
try doStop(name: name, signal: "SIGKILL")
|
||||
let status = try getContainerStatus(name)
|
||||
#expect(status == "stopped")
|
||||
|
||||
// Stopping an already stopped container should not fail.
|
||||
try doStop(name: name, signal: "SIGKILL")
|
||||
}
|
||||
}
|
||||
@@ -329,13 +329,13 @@ class CLITest {
|
||||
return resp
|
||||
}
|
||||
|
||||
func doStop(name: String, signal: String = "SIGKILL") throws {
|
||||
let (_, _, error, status) = try run(arguments: [
|
||||
"stop",
|
||||
"-s",
|
||||
signal,
|
||||
name,
|
||||
])
|
||||
func doStop(name: String, signal: String? = "SIGKILL") throws {
|
||||
var arguments = ["stop"]
|
||||
if let signal {
|
||||
arguments.append(contentsOf: ["-s", signal])
|
||||
}
|
||||
arguments.append(name)
|
||||
let (_, _, error, status) = try run(arguments: arguments)
|
||||
if status != 0 {
|
||||
throw CLIError.executionFailed("command failed: \(error)")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user