Skip to content

Commit be89df8

Browse files
authored
Make "Plugin not found" message more descriptive (apple#569)
Users frequently saw “failed to find plugin …” when the system services weren’t running; the message didn’t explain the root cause or where plugins are looked up. No change to plugin execution flow; only error messaging and path discovery hints are improved for a better UX.
1 parent 8f6f39e commit be89df8

File tree

2 files changed

+78
-1
lines changed

2 files changed

+78
-1
lines changed

Sources/CLI/DefaultCommand.swift

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import ArgumentParser
1818
import ContainerClient
1919
import ContainerPlugin
2020
import Darwin
21+
import Foundation
2122

2223
struct DefaultCommand: AsyncParsableCommand {
2324
static let configuration = CommandConfiguration(
@@ -46,8 +47,50 @@ struct DefaultCommand: AsyncParsableCommand {
4647
throw ValidationError("Unknown option '\(command)'")
4748
}
4849

50+
// Compute canonical plugin directories to show in helpful errors (avoid hard-coded paths)
51+
let installRoot = CommandLine.executablePathUrl
52+
.deletingLastPathComponent()
53+
.appendingPathComponent("..")
54+
.standardized
55+
let userPluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot)
56+
let installRootPluginsURL =
57+
installRoot
58+
.appendingPathComponent("libexec")
59+
.appendingPathComponent("container")
60+
.appendingPathComponent("plugins")
61+
.standardized
62+
let hintPaths = [userPluginsURL, installRootPluginsURL]
63+
.map { $0.appendingPathComponent(command).path(percentEncoded: false) }
64+
.joined(separator: "\n - ")
65+
66+
// If plugin loader couldn't be created, the system/APIServer likely isn't running.
67+
if pluginLoader == nil {
68+
throw ValidationError(
69+
"""
70+
Plugins are unavailable. Start the container system services and retry:
71+
72+
container system start
73+
74+
Check to see that the plugin exists under:
75+
- \(hintPaths)
76+
77+
"""
78+
)
79+
}
80+
4981
guard let plugin = pluginLoader?.findPlugin(name: command), plugin.config.isCLI else {
50-
throw ValidationError("failed to find plugin named container-\(command)")
82+
throw ValidationError(
83+
"""
84+
Plugin 'container-\(command)' not found.
85+
86+
- If system services are not running, start them with: container system start
87+
- If the plugin isn't installed, ensure it exists under:
88+
89+
Check to see that the plugin exists under:
90+
- \(hintPaths)
91+
92+
"""
93+
)
5194
}
5295
// Before execing into the plugin, restore default SIGINT/SIGTERM so the plugin can manage signals.
5396
Self.resetSignalsForPluginExec()
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//===----------------------------------------------------------------------===//
2+
// Copyright © 2025 Apple Inc. and the container project authors. All rights reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// https://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
//===----------------------------------------------------------------------===//
16+
17+
import Testing
18+
19+
struct TestCLIPluginErrors {
20+
@Test
21+
func testHelpfulMessageWhenPluginsUnavailable() throws {
22+
// Intentionally invoke an unknown plugin command. In CI this should run
23+
// without the APIServer started, so DefaultCommand will fail to create
24+
// a PluginLoader and emit the improved guidance.
25+
let cli = try CLITest()
26+
let (_, stderr, status) = try cli.run(arguments: ["nosuchplugin"]) // non-existent plugin name
27+
28+
#expect(status != 0)
29+
#expect(stderr.contains("container system start"))
30+
#expect(stderr.contains("Plugins are unavailable") || stderr.contains("Plugin 'container-"))
31+
// Should include at least one computed plugin search path hint
32+
#expect(stderr.contains("container-plugins") || stderr.contains("container/plugins"))
33+
}
34+
}

0 commit comments

Comments
 (0)