From be89df833bfaa8e593b088728991f520d621d5c2 Mon Sep 17 00:00:00 2001 From: mazdak Date: Mon, 8 Sep 2025 19:13:23 -0400 Subject: [PATCH] Make "Plugin not found" message more descriptive (#569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Sources/CLI/DefaultCommand.swift | 45 ++++++++++++++++++- .../Plugins/TestCLIPluginErrors.swift | 34 ++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 Tests/CLITests/Subcommands/Plugins/TestCLIPluginErrors.swift diff --git a/Sources/CLI/DefaultCommand.swift b/Sources/CLI/DefaultCommand.swift index 30b55d1d..20a321f1 100644 --- a/Sources/CLI/DefaultCommand.swift +++ b/Sources/CLI/DefaultCommand.swift @@ -18,6 +18,7 @@ import ArgumentParser import ContainerClient import ContainerPlugin import Darwin +import Foundation struct DefaultCommand: AsyncParsableCommand { static let configuration = CommandConfiguration( @@ -46,8 +47,50 @@ struct DefaultCommand: AsyncParsableCommand { throw ValidationError("Unknown option '\(command)'") } + // Compute canonical plugin directories to show in helpful errors (avoid hard-coded paths) + let installRoot = CommandLine.executablePathUrl + .deletingLastPathComponent() + .appendingPathComponent("..") + .standardized + let userPluginsURL = PluginLoader.userPluginsDir(installRoot: installRoot) + let installRootPluginsURL = + installRoot + .appendingPathComponent("libexec") + .appendingPathComponent("container") + .appendingPathComponent("plugins") + .standardized + let hintPaths = [userPluginsURL, installRootPluginsURL] + .map { $0.appendingPathComponent(command).path(percentEncoded: false) } + .joined(separator: "\n - ") + + // If plugin loader couldn't be created, the system/APIServer likely isn't running. + if pluginLoader == nil { + throw ValidationError( + """ + Plugins are unavailable. Start the container system services and retry: + + container system start + + Check to see that the plugin exists under: + - \(hintPaths) + + """ + ) + } + guard let plugin = pluginLoader?.findPlugin(name: command), plugin.config.isCLI else { - throw ValidationError("failed to find plugin named container-\(command)") + throw ValidationError( + """ + Plugin 'container-\(command)' not found. + + - If system services are not running, start them with: container system start + - If the plugin isn't installed, ensure it exists under: + + Check to see that the plugin exists under: + - \(hintPaths) + + """ + ) } // Before execing into the plugin, restore default SIGINT/SIGTERM so the plugin can manage signals. Self.resetSignalsForPluginExec() diff --git a/Tests/CLITests/Subcommands/Plugins/TestCLIPluginErrors.swift b/Tests/CLITests/Subcommands/Plugins/TestCLIPluginErrors.swift new file mode 100644 index 00000000..2086fdb6 --- /dev/null +++ b/Tests/CLITests/Subcommands/Plugins/TestCLIPluginErrors.swift @@ -0,0 +1,34 @@ +//===----------------------------------------------------------------------===// +// 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 Testing + +struct TestCLIPluginErrors { + @Test + func testHelpfulMessageWhenPluginsUnavailable() throws { + // Intentionally invoke an unknown plugin command. In CI this should run + // without the APIServer started, so DefaultCommand will fail to create + // a PluginLoader and emit the improved guidance. + let cli = try CLITest() + let (_, stderr, status) = try cli.run(arguments: ["nosuchplugin"]) // non-existent plugin name + + #expect(status != 0) + #expect(stderr.contains("container system start")) + #expect(stderr.contains("Plugins are unavailable") || stderr.contains("Plugin 'container-")) + // Should include at least one computed plugin search path hint + #expect(stderr.contains("container-plugins") || stderr.contains("container/plugins")) + } +}