diff --git a/examples/plugin/README.md b/examples/plugin/README.md new file mode 100644 index 00000000..086d7f00 --- /dev/null +++ b/examples/plugin/README.md @@ -0,0 +1,139 @@ +# PingCLI Plugin Development Guide + +Welcome to the developer guide for creating `pingcli` plugins! This document provides all the information you need to build, test, and distribute your own custom commands to extend the functionality of the `pingcli` tool. + +## Table of Contents + +- [Introduction](#introduction) +- [Prerequisites](#prerequisites) +- [How Plugins Work](#how-plugins-work) +- [Building a Plugin](#building-a-plugin) +- [Registering and Managing Plugins](#registering-and-managing-plugins) + - [Adding a Plugin](#adding-a-plugin) + - [Listing Plugins](#listing-plugins) + - [Removing a Plugin](#removing-a-plugin) +- [Plugin Command Interface](#plugin-command-interface) + - [Configuration](#configuration-pingclicommandconfiguration-error) + - [Run](#runargs-string-logger-grpclogger-error) +- [Logging from Plugins](#logging-from-plugins) +- [Troubleshooting](#troubleshooting) +- [Further Reading](#further-reading) + +## Introduction + +The `pingcli` plugin system allows developers to create new commands that integrate seamlessly into the main application. Each plugin is a standalone executable that communicates with the `pingcli` host process over gRPC. This architecture ensures that plugins are isolated and secure. Currently, the plugin framework only supports plugins written in Go. + +## Prerequisites + +- **Go 1.24+** (for building Go plugins) +- [HashiCorp go-plugin](https://github.com/hashicorp/go-plugin) (used by both host and plugin) +- **PingCLI v0.7.0+** installed and configured + +## How Plugins Work + +1. **Discovery**: When `pingcli` starts, it loads a list of registered plugin executables from its configuration profile. This list is managed by the `pingcli plugin` command. Plugins must be on your system `PATH` +2. **Handshake**: For each registered plugin, `pingcli` launches the executable as a child process. A secure handshake is performed to verify that the child process is a valid plugin and is compatible with the host. +3. **Communication**: Once the handshake is complete, the host and plugin communicate over gRPC. The host can call functions defined in the plugin (like `Run`), and the plugin can send log messages back to the host. +4. **Execution**: When a user runs a command provided by a plugin, `pingcli` invokes the corresponding gRPC method in the plugin process, passing along any arguments and flags. +5. **Compatibility**: The Handshake process includes a ProtocolVersion check. This ensures that the plugin and the pingcli host are compatible, preventing issues if the underlying gRPC interface changes in future versions of pingcli. + +## Building a Plugin + +1. **Clone or create your plugin source code.** + See [`plugin.go`](plugin.go) for a complete example. + +2. **Build the plugin binary:** + ```sh + go build -o my-plugin + ``` + +3. **Place the binary in a directory on your `PATH`:** + ```sh + mv my-plugin ~/go/bin/ + # or any directory in your $PATH + ``` + +## Registering and Managing Plugins + +`pingcli` provides the `plugin` command to manage the lifecycle of your plugins. + +### Adding a Plugin + +To add a new plugin, use the add subcommand. Crucially, the plugin executable must first be placed in a directory that is part of your system's PATH environment variable. pingcli relies on the system's PATH to find the executable to run. + +```bash +pingcli plugin add +``` + +### Listing Plugins + +To see a list of all currently registered plugins, use the `list` subcommand. + +```bash +pingcli plugin list +``` + +### Removing a Plugin + +To unregister a plugin from `pingcli`, use the `remove` subcommand. + +```bash +pingcli plugin remove +``` + +## Plugin Command Interface + +To create a valid plugin, you must implement the `grpc.PingCliCommand` interface. This interface has two methods: + +#### `Configuration() (*grpc.PingCliCommandConfiguration, error)` + +This method is called by the `pingcli` host to get metadata about your command. This allows `pingcli` to display your command in the help text (`pingcli --help`). + +The `PingCliCommandConfiguration` struct has the following fields, which correspond directly to properties of a [Cobra](https://github.com/spf13/cobra) command: + +- `Use`: The one-line usage message for the command (e.g., `my-command [flags]`). +- `Short`: A short description of the command. +- `Long`: A longer, more detailed description of the command. +- `Example`: One or more examples of how to use the command. + +By providing this metadata, pingcli can present your plugin in a manner that is consistent and feels native to the main application. + +#### `Run(args []string, logger grpc.Logger) error` + +This is the main entry point for your command's logic. It is executed when a user runs your command. + +- `args []string`: A slice of strings containing all the command-line arguments and flags that were passed to your command. For example, if a user runs `pingcli my-command first-arg --verbose`, the `args` slice will be `["first-arg", "--verbose"]`. +- `logger grpc.Logger`: A gRPC client that allows your plugin to send log messages back to the `pingcli` host. **This is the only way your plugin should produce output.** + +## Logging from Plugins + +Plugins must not write directly to `stdout` or `stderr`. Instead, they must use the provided `logger` object in the `Run` method. This ensures that all output is managed by the host and presented to the user in a consistent format. + +The `logger` interface provides several methods for different log levels: + +- `logger.Message(message string, fields map[string]string)` +- `logger.Warn(message string, fields map[string]string)` +- `logger.PluginError(message string, fields map[string]string)` +- `logger.Success(message string, fields map[string]string)` +- `logger.UserError(message string, fields map[string]string)` +- `logger.UserFatal(message string, fields map[string]string)` + +## Troubleshooting + +- **Plugin not found:** + Ensure the binary is on your `PATH` and registered with `pingcli plugin add`. + +- **Handshake failed:** + Check that both host and plugin use compatible protocol versions. + +- **gRPC errors:** + Ensure your plugin implements the correct interface and uses the expected gRPC protocol. + +- **No output:** + All output is expected to go through the provided `logger`. + +## Further Reading + +- [HashiCorp go-plugin documentation](https://github.com/hashicorp/go-plugin) +- [`pingcli` main documentation](../../README.md) +- [Cobra CLI framework](https://github.com/spf13/cobra) diff --git a/examples/plugin/plugin.go b/examples/plugin/plugin.go new file mode 100644 index 00000000..1f8cf927 --- /dev/null +++ b/examples/plugin/plugin.go @@ -0,0 +1,115 @@ +// Copyright © 2025 Ping Identity Corporation + +// Package 'plugin' provides an example implementation of a PingCLI command plugin. +// +// It demonstrates the required structure and interfaces for building a new +// command that can be dynamically loaded and executed by the main pingcli +// application. This includes implementing the PingCliCommand interface and +// serving it over gRPC using Hashicorp's `go-plugin“ library. +package plugin + +import ( + "github.com/hashicorp/go-plugin" + "github.com/pingidentity/pingcli/shared/grpc" +) + +// These variables define the command's metadata, which is sent to the pingcli +// host process. This information is used by the host's command-line framework +// (Cobra) to display help text, usage, and examples, making the plugin feel +// like a native command. +var ( + // Example provides one or more usage examples for the command. + Example = "pingcli plugin-command --flag value" + + // Long provides a detailed description of the command. It's shown in the + // help text when a user runs `pingcli help plugin-command`. + Long = `This command is an example of a plugin command that can be used with pingcli. + It demonstrates how to implement a custom command that can be executed by the pingcli host process` + + // Short provides a brief, one-line description of the command. + Short = "An example plugin command for pingcli" + + // Use defines the command's name and its arguments/flags syntax. + Use = "plugin-command [flags]" +) + +// PingCliCommand is the implementation of the grpc.PingCliCommand interface. +// It encapsulates the logic for the custom command provided by this plugin. +type PingCliCommand struct{} + +// A compile-time check to ensure PingCliCommand correctly implements the +// grpc.PingCliCommand interface. +var _ grpc.PingCliCommand = (*PingCliCommand)(nil) + +// Configuration is called by the pingcli host to retrieve the command's +// metadata, such as its name, description, and usage examples. This allows +// the host to integrate the plugin's command into its own help and usage +// displays without executing the plugin's main logic. +func (c *PingCliCommand) Configuration() (*grpc.PingCliCommandConfiguration, error) { + cmdConfig := &grpc.PingCliCommandConfiguration{ + Example: Example, + Long: Long, + Short: Short, + Use: Use, + } + + return cmdConfig, nil +} + +// Run is the execution entry point for the plugin command. The pingcli host +// calls this method when a user invokes the plugin command. +// +// The `args` parameter contains all command-line arguments and flags passed +// after the command's name (as defined in the `Use` variable). For example, +// if a user runs `pingcli plugin-command add --flag value`, the `args` slice +// will be `["add", "--flag", "value"]`. +// +// The `logger` parameter is a gRPC client that allows the plugin to send log +// messages back to the host process, ensuring that all output is displayed +// consistently through the main pingcli interface. +func (c *PingCliCommand) Run(args []string, logger grpc.Logger) error { + err := logger.Message("Message from plugin", nil) + if err != nil { + return err + } + + err = logger.Warn("Warning from plugin", nil) + if err != nil { + return err + } + + err = logger.PluginError("Error from plugin", map[string]string{ + "key": "value", + "debug": "info", + }) + if err != nil { + return err + } + + return nil +} + +// main is the entry point for the plugin's executable. When the pingcli host +// launches this plugin, this function starts a gRPC server that serves the +// PingCliCommand implementation. The go-plugin library handles the handshake +// and communication between the host and the plugin process. +func main() { //nolint:unused + plugin.Serve(&plugin.ServeConfig{ + // HandshakeConfig is a shared configuration used to verify that the host + // and plugin are compatible. + HandshakeConfig: grpc.HandshakeConfig, + + // Plugins defines the set of services this plugin serves. The key is a + // unique name for the plugin service, and the value is an implementation + // of the plugin.Plugin interface. + Plugins: map[string]plugin.Plugin{ + grpc.ENUM_PINGCLI_COMMAND_GRPC: &grpc.PingCliCommandGrpcPlugin{ + Impl: &PingCliCommand{}, + }, + }, + + // GRPCServer specifies the gRPC server implementation to use. + // plugin.DefaultGRPCServer is a sane default provided by the library. + GRPCServer: plugin.DefaultGRPCServer, + }) +} diff --git a/go.mod b/go.mod index c684b2c4..13edd643 100644 --- a/go.mod +++ b/go.mod @@ -12,16 +12,16 @@ require ( github.com/hashicorp/go-hclog v1.6.3 github.com/hashicorp/go-plugin v1.6.3 github.com/hashicorp/go-uuid v1.0.3 - github.com/knadh/koanf/parsers/yaml v1.0.0 + github.com/knadh/koanf/parsers/yaml v1.1.0 github.com/knadh/koanf/providers/confmap v1.0.0 github.com/knadh/koanf/providers/file v1.2.0 github.com/knadh/koanf/v2 v2.2.1 github.com/manifoldco/promptui v0.9.0 - github.com/patrickcping/pingone-go-sdk-v2 v0.12.17 - github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.0 - github.com/patrickcping/pingone-go-sdk-v2/management v0.57.0 - github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.0 - github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.0 + github.com/patrickcping/pingone-go-sdk-v2 v0.13.0 + github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.1 + github.com/patrickcping/pingone-go-sdk-v2/management v0.59.0 + github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.1 + github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.1 github.com/pingidentity/pingfederate-go-client/v1220 v1220.0.0 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.9.1 @@ -160,8 +160,8 @@ require ( github.com/nunnatsa/ginkgolinter v0.19.1 // indirect github.com/oklog/run v1.0.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.0 // indirect - github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.0 // indirect + github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.1 // indirect + github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.1 // indirect github.com/pavius/impi v0.0.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -222,6 +222,7 @@ require ( go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.9.0 // indirect go.uber.org/zap v1.24.0 // indirect + go.yaml.in/yaml/v3 v3.0.3 // indirect golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect golang.org/x/net v0.39.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect diff --git a/go.sum b/go.sum index 285c41f2..17e41296 100644 --- a/go.sum +++ b/go.sum @@ -373,8 +373,8 @@ github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/tt github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= -github.com/knadh/koanf/parsers/yaml v1.0.0 h1:PXyeHCRhAMKyfLJaoTWsqUTxIFeDMmdAKz3XVEslZV4= -github.com/knadh/koanf/parsers/yaml v1.0.0/go.mod h1:Q63VAOh/s6XaQs6a0TB2w9GFUuuPGvfYrCSWb9eWAQU= +github.com/knadh/koanf/parsers/yaml v1.1.0 h1:3ltfm9ljprAHt4jxgeYLlFPmUaunuCgu1yILuTXRdM4= +github.com/knadh/koanf/parsers/yaml v1.1.0/go.mod h1:HHmcHXUrp9cOPcuC+2wrr44GTUB0EC+PyfN3HZD9tFg= github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A= github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U= @@ -483,20 +483,20 @@ github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJ github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/patrickcping/pingone-go-sdk-v2 v0.12.17 h1:XN4IpfaqGVTcmNYx9Qh42Yo9TJLCZGl+sQT5H7sUnBA= -github.com/patrickcping/pingone-go-sdk-v2 v0.12.17/go.mod h1:6Q+d1SXedSku9+64HlgZofHGTfahQmoGKRwMsWBn7qM= -github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.0 h1:gEPzZToJlBcJh2Ft12dP1GCSGzsNFQFEHS7Bql86RQk= -github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.0/go.mod h1:2PDrgC1ufXk2IDIk4JQHx6r34r2xpkbnzKIpXFv8gYs= -github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.0 h1:pLiiBkROks/40vhFWJEcr/tiIEqqYdP4FWsHtfCLdIs= -github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.0/go.mod h1:yRGf7+tsB3/AQYsNjIIs4ScJhR885mvDYMgwHiQeMl0= -github.com/patrickcping/pingone-go-sdk-v2/management v0.57.0 h1:no242yX2sr9BIyWoyuBGzVscZJgDaE0RseUJL0AmqPk= -github.com/patrickcping/pingone-go-sdk-v2/management v0.57.0/go.mod h1:oLB/jjAkn4oEA60nC5/0KAobvcNJbflOWnVaS6lKxv8= -github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.0 h1:k133OY6PNO3tgNK3LBoEI+Uf9bRNKsvAkMMVUf99/Q0= -github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.0/go.mod h1:Q+Ym6kktv5Y6VnVhDt//lWoOhmIKfyjo6ejRx5mLttY= -github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.0 h1:qGdwnfjsexHhTUAyBaUzheyeKWhR3Q8groqVpprzzOw= -github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.0/go.mod h1:ppwkDT/w2/2y2aFH+hFQgziLMsWvz2MEZvwYexREqRk= -github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.0 h1:Gnxvi7yx4NSBNOqBBydUPoR9Flp/dnnXj3129+ub9WY= -github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.0/go.mod h1:bCq5fHv9mSdNsm/XiT5jb3YgYnQb8F824EYfq9eAJl4= +github.com/patrickcping/pingone-go-sdk-v2 v0.13.0 h1:0GUXULyb6VdYv6pLXplPcA3UloamGqooz8niZCYCwis= +github.com/patrickcping/pingone-go-sdk-v2 v0.13.0/go.mod h1:VU2guylo0C4Bad/4iRBAx7nR39+uTaki4YbkRCugs5Y= +github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.1 h1:Q4lJI8jmK1/HYy82cb4FHCh6cxNebkYiioRgsf8SX8I= +github.com/patrickcping/pingone-go-sdk-v2/authorize v0.8.1/go.mod h1:2PDrgC1ufXk2IDIk4JQHx6r34r2xpkbnzKIpXFv8gYs= +github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.1 h1:p7QsW+80U5LM43gkJYetYFPNeRmW5k8FteWMvEyPsJk= +github.com/patrickcping/pingone-go-sdk-v2/credentials v0.11.1/go.mod h1:yRGf7+tsB3/AQYsNjIIs4ScJhR885mvDYMgwHiQeMl0= +github.com/patrickcping/pingone-go-sdk-v2/management v0.59.0 h1:VWvxsNdYvOvfcbsA7Wbdyd4x9Zs+FlVs39PQ0F9yQ2M= +github.com/patrickcping/pingone-go-sdk-v2/management v0.59.0/go.mod h1:oLB/jjAkn4oEA60nC5/0KAobvcNJbflOWnVaS6lKxv8= +github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.1 h1:M97mqgeECxnF27syT+2dTYCSMFvGAZ+JFYXRsuKMlwY= +github.com/patrickcping/pingone-go-sdk-v2/mfa v0.23.1/go.mod h1:Q+Ym6kktv5Y6VnVhDt//lWoOhmIKfyjo6ejRx5mLttY= +github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.1 h1:sthTrmwt46h9ey1TWI8B8LKctefuohvCXNdRXeh+ra0= +github.com/patrickcping/pingone-go-sdk-v2/risk v0.19.1/go.mod h1:ppwkDT/w2/2y2aFH+hFQgziLMsWvz2MEZvwYexREqRk= +github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.1 h1:Os9QKUELVNTi+sFsflANjWZplc01K/wxetEsiAacQBc= +github.com/patrickcping/pingone-go-sdk-v2/verify v0.9.1/go.mod h1:bCq5fHv9mSdNsm/XiT5jb3YgYnQb8F824EYfq9eAJl4= github.com/pavius/impi v0.0.3 h1:DND6MzU+BLABhOZXbELR3FU8b+zDgcq4dOCNLhiTYuI= github.com/pavius/impi v0.0.3/go.mod h1:x/hU0bfdWIhuOT1SKwiJg++yvkk6EuOtJk8WtDZqgr8= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -703,6 +703,8 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= +go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= diff --git a/internal/plugins/plugins.go b/internal/plugins/plugins.go index f9929d6e..89e5cdaa 100644 --- a/internal/plugins/plugins.go +++ b/internal/plugins/plugins.go @@ -1,7 +1,8 @@ +// Copyright © 2025 Ping Identity Corporation + package plugins import ( - "errors" "fmt" "io" "os/exec" @@ -85,11 +86,11 @@ func createHPluginClient(pluginExecutable string) *hplugin.Client { // dispensePlugin connects to the plugin via RPC and dispenses the grpc.PingCliCommand interface. // the caller is responsible for closing the client connection after use. -func dispensePlugin(client *hplugin.Client, pluginExecutable string) (hplugin.ClientProtocol, grpc.PingCliCommand, error) { +func dispensePlugin(client *hplugin.Client, pluginExecutable string) (grpc.PingCliCommand, error) { // Connect via RPC clientProtocol, err := client.Client() if err != nil { - return nil, nil, fmt.Errorf("failed to create Plugin RPC client: %w", err) + return nil, fmt.Errorf("failed to create Plugin RPC client: %w", err) } // All PingCLI plugins are expected to serve the ENUM_PINGCLI_COMMAND_GRPC plugin via @@ -98,31 +99,24 @@ func dispensePlugin(client *hplugin.Client, pluginExecutable string) (hplugin.Cl // raw value of ENUM_PINGCLI_COMMAND_GRPC "pingcli_command_grpc" for the PluginMap key. raw, err := clientProtocol.Dispense(grpc.ENUM_PINGCLI_COMMAND_GRPC) if err != nil { - return nil, nil, fmt.Errorf("the rpc client failed to dispense plugin executable '%s': %w", pluginExecutable, err) + return nil, fmt.Errorf("the rpc client failed to dispense plugin executable '%s': %w", pluginExecutable, err) } // Cast the dispensed plugin to the interface we expect to work with: grpc.PingCliCommand. // However, this is not a normal interface, but rather implemeted over the RPC connection. plugin, ok := raw.(grpc.PingCliCommand) if !ok { - return nil, nil, fmt.Errorf("failed to cast plugin executable '%s' to grpc.PingCliCommand interface", pluginExecutable) + return nil, fmt.Errorf("failed to cast plugin executable '%s' to grpc.PingCliCommand interface", pluginExecutable) } - return clientProtocol, plugin, nil + return plugin, nil } func pluginConfiguration(pluginExecutable string) (conf *grpc.PingCliCommandConfiguration, err error) { client := createHPluginClient(pluginExecutable) defer client.Kill() - clientProtocol, plugin, err := dispensePlugin(client, pluginExecutable) - defer func() { - cErr := clientProtocol.Close() - if cErr != nil { - err = errors.Join(err, cErr) - } - }() - + plugin, err := dispensePlugin(client, pluginExecutable) if err != nil { return nil, err } @@ -145,14 +139,7 @@ func createCmdRunE(pluginExecutable string) func(cmd *cobra.Command, args []stri client := createHPluginClient(pluginExecutable) defer client.Kill() - clientProtocol, plugin, err := dispensePlugin(client, pluginExecutable) - defer func() { - cErr := clientProtocol.Close() - if cErr != nil { - err = errors.Join(err, cErr) - } - }() - + plugin, err := dispensePlugin(client, pluginExecutable) if err != nil { return err } diff --git a/internal/profiles/koanf.go b/internal/profiles/koanf.go index acce2065..94215064 100644 --- a/internal/profiles/koanf.go +++ b/internal/profiles/koanf.go @@ -70,9 +70,13 @@ func KoanfValueFromOption(opt options.Option, pName string) (value string, ok bo mainKoanfInstance = GetKoanfConfig() ) + if mainKoanfInstance == nil || mainKoanfInstance.KoanfInstance() == nil { + return "", false, fmt.Errorf("failed to get option value: koanf instance is not initialized") + } + // Case 1: Koanf Key is the ActiveProfile Key, get value from main koanf instance - if opt.KoanfKey != "" && strings.EqualFold(opt.KoanfKey, options.RootActiveProfileOption.KoanfKey) && mainKoanfInstance != nil { - kValue = mainKoanfInstance.KoanfInstance().Get("activeprofile") + if strings.EqualFold(opt.KoanfKey, options.RootActiveProfileOption.KoanfKey) { + kValue = mainKoanfInstance.KoanfInstance().Get(options.RootActiveProfileOption.KoanfKey) if kValue == nil { kValue = mainKoanfInstance.KoanfInstance().Get(opt.KoanfKey) } diff --git a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_domain.go b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_domain.go index 23395d94..f4fbcf7d 100644 --- a/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_domain.go +++ b/internal/testing/testutils_resource/pingone_platform_testable_resources/custom_domain.go @@ -36,7 +36,7 @@ func createCustomDomain(t *testing.T, clientInfo *connector.ClientInfo, resource request := clientInfo.PingOneApiClient.ManagementAPIClient.CustomDomainsApi.CreateDomain(clientInfo.PingOneContext, clientInfo.PingOneExportEnvironmentID) clientStruct := management.CustomDomain{ - DomainName: "custom-domain.example.com", + DomainName: "custom-domain.pingcli-test.com", } request = request.CustomDomain(clientStruct)