README
¶
xp-clifford
About this project
xp-clifford (Crossplane CLI Framework for Resource Data Extraction) is a Go module that facilitates the development of CLI tools for exporting definitions of external resources in the format of specific Crossplane provider managed resource definitions.
The resource definitions can then be imported into Crossplane using the standard import procedure. It is recommended to check the generated definitions for comments, before doing the import. See also Exporting commented out resources.
Requirements
xp-clifford is a Go module and requires only a working Go development environment.
Setup
To install the xp-clifford Go module, run the following command:
go get github.com/SAP/xp-clifford
Support, Feedback, Contributing
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
Security / Disclosure
If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.
Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Licensing
Copyright 2026 SAP SE or an SAP affiliate company and xp-clifford contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.
Examples
These examples demonstrate the basic features of xp-clifford and build progressively on one another.
The simplest CLI tool
The simplest CLI tool you can create using xp-clifford looks like this:
package main
import (
"github.com/SAP/xp-clifford/cli"
_ "github.com/SAP/xp-clifford/cli/export"
)
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
cli.Execute()
}
Let's examine the import section.
import (
"github.com/SAP/xp-clifford/cli"
_ "github.com/SAP/xp-clifford/cli/export"
)
Two packages must be imported:
github.com/SAP/xp-clifford/cligithub.com/SAP/xp-clifford/cli/export
The cli/export package is imported for side effects only.
The main function looks like this:
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
cli.Execute()
}
The Configuration variable from the cli package is used to set specific parameters for the built CLI tool. Here we set the ShortName and ObservedSystem fields.
These fields have the following meanings:
- ShortName: The abbreviated name of the observed system without spaces, such as "cf" for the CloudFoundry provider
- ObservedSystem: The full name of the external system, which may contain spaces, such as "Cloud Foundry"
At the end of the main function, we invoke the Execute function from the cli package to start the CLI.
When we run this basic example, it generates the following output:
go run ./examples/basic/main.go
test system exporting tool is a CLI tool for exporting existing resources as Crossplane managed resources
Usage:
test-exporter [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
export Export test system resources
help Help about any command
Flags:
-c, --config string Configuration file
-h, --help help for test-exporter
-v, --verbose Verbose output
Use "test-exporter [command] --help" for more information about a command.
If you try running the CLI tool with the export subcommand, you get an error message.
go run ./examples/basic/main.go export
ERRO export subcommand is not set
Exporting
Basic export subcommand
The export subcommand is mandatory, but you are responsible for implementing the code that executes when it is invoked.
The code must be defined as a function with the following signature:
func(ctx context.Context, events export.EventHandler) error
The ctx parameter can be used to handle interruptions, such as when the user presses Ctrl-C. In such cases, the Done() channel of the context is closed.
The events parameter from the export package provides three methods for communicating progress to the CLI framework:
- Warn: Indicates a recoverable error that does not terminate the export operation.
- Resource: Indicates a processed managed resource to be printed or stored by the export operation.
- Stop: Indicates that exporting has finished. No more
WarnorResourcecalls should be made afterStop.
A fatal error can be indicated by returning a non-nil error value.
A simple implementation of an export logic function looks like this:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
events.Stop()
return nil
}
This implementation prints a log message, stops the event handler, and returns a nil error value.
You can configure the business logic function using the SetCommand function from the export package:
export.SetCommand(exportLogic)
A complete example is:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
events.Stop()
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
To invoke the export subcommand:
go run ./examples/export/main.go export
INFO export command invoked
Exporting a resource
In the previous example, we created a proper export subcommand, but didn't actually export any resources.
To export a resource, use the Resource method of the EventHandler type:
Resource(res resource.Object) // Object interface defined in
// github.com/crossplane/crossplane-runtime/pkg/resource
This method accepts a resource.Object, an interface implemented by all Crossplane resources.
Let's update our exportLogic function to export a single resource. For simplicity, we'll use the Unstructured type from k8s.io/apimachinery/pkg/apis/meta/v1/unstructured, which implements the resource.Object interface:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user",
"password": "secret",
},
}
events.Resource(res)
events.Stop()
return nil
}
The complete example now looks like this:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user",
"password": "secret",
},
}
events.Resource(res)
events.Stop()
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Running this example produces the following output:
go run ./examples/exportsingle/main.go export
INFO export command invoked
---
password: secret
user: test-user
...
The exported resource is printed to the console. You can redirect the output to a file using the -o flag:
go run ./examples/exportsingle/main.go export -o output.yaml
INFO export command invoked
INFO Writing output to file output=output.yaml
The output.yaml file contains the exported resource object:
cat output.yaml
---
password: secret
user: test-user
...
Displaying warnings
During the processing and conversion of external resources, the export logic may encounter unexpected situations such as unstable network connections, authentication issues, or unknown resource configurations.
These events should not halt the resource export process, but they must be reported to the user.
You can report warnings using the Warn method of the EventHandler type:
Warn(err error)
The Warn method supports erratt.Error types. The erratt.Error type is demonstrated in 6.3.
Let's add a warning message to our exportLogic function:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
events.Warn(errors.New("generating test resource"))
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user-with-warning",
"password": "secret",
},
}
events.Resource(res)
events.Stop()
return nil
}
The complete example now looks like this:
package main
import (
"context"
"errors"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
events.Warn(errors.New("generating test resource"))
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user-with-warning",
"password": "secret",
},
}
events.Resource(res)
events.Stop()
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Running this example displays the warning message in the logs:
go run ./examples/exportwarn/main.go export
INFO export command invoked
WARN generating test resource
---
password: secret
user: test-user-with-warning
...
When redirecting the output to a file, the warning appears on screen but not in the file:
go run ./examples/exportwarn/main.go export -o output.yaml
INFO export command invoked
WARN generating test resource
INFO Writing output to file output=output.yaml
cat output.yaml
---
password: secret
user: test-user-with-warning
...
Exporting commented out resources
During the export process, problems may prevent generation of valid managed resource definitions, or the definitions produced may be unsafe to apply.
You have two options for handling problematic resources: omit them from the output entirely, or include them but commented out. Commenting out invalid or unsafe resource definitions ensures users won't encounter problems when applying the export tool output.
xp-clifford comments out resources that implement the yaml.CommentedYAML interface, which defines a single method:
type CommentedYAML interface {
Comment() (string, bool)
}
The bool return value indicates whether the managed resource should be commented out. The string return value provides a message that will be printed as part of the comment.
Since Crossplane managed resources don't typically implement the CommentedYAML interface, you can wrap them to add this functionality.
The yaml.NewResourceWithComment function handles this wrapping for you:
func NewResourceWithComment(res resource.Object) *yaml.ResourceWithComment
The *yaml.ResourceWithComment type wraps res and implements the yaml.CommentedYAML interface. It also provides helper methods:
- SetComment: sets the comment string
- AddComment: appends to the comment string
The following example demonstrates the commenting feature:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user-commented",
"password": "secret",
},
}
commentedResource := yaml.NewResourceWithComment(res)
commentedResource.SetComment("don't deploy it, this is a test resource!")
events.Resource(commentedResource)
events.Stop()
return nil
}
Here is the complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"github.com/SAP/xp-clifford/yaml"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
res := &unstructured.Unstructured{
Object: map[string]interface{}{
"user": "test-user-commented",
"password": "secret",
},
}
commentedResource := yaml.NewResourceWithComment(res)
commentedResource.SetComment("don't deploy it, this is a test resource!")
events.Resource(commentedResource)
events.Stop()
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Running this example displays the commented resource with its comment message:
go run ./examples/exportcomment/main.go export
INFO export command invoked
#
# don't deploy it, this is a test resource!
#
# ---
# password: secret
# user: test-user-commented
# ...
This works equally well when redirecting output to a file using the -o flag.
Errors with attributes
The erratt package implements a new error type designed for efficient use with the Warn method of EventHandler.
The erratt.Error type implements the standard Go error interface. Additionally, it can be extended with slog package compatible key-value pairs used for structured logging. The erratt.Error type also supports wrapping Go error values. When an erratt.Error is wrapped, its attributes are preserved.
You can create a simple erratt.Error using the erratt.New function:
err := erratt.New("something went wrong")
errWithAttrs1 := erratt.New("error opening file", "filename", filename)
errWithAttrs2 := erratt.New("authentication failed", "username", user, "password", pass)
In this example, errWithAttrs1 and errWithAttrs2 include additional attributes.
You can wrap an existing error value using the erratt.Errorf function:
err := callFunction()
errWrapped := erratt.Errorf("unexpected error occurred: %w", err)
You can extend an erratt.Error value with attributes using the With method:
err := connectToServer(url, username, password)
errWrapped := erratt.Errorf("cannot connect to server: %w", err).
With("url", url, "username", username, "password", password)
For a complete example, consider two functions that return erratt.Error values and demonstrate wrapping:
func auth() erratt.Error {
return erratt.New("authentication failure",
"username", "test-user",
"password", "test-password",
)
}
func connect() erratt.Error {
err := auth()
if err != nil {
return erratt.Errorf("connect failed: %w", err).
With("url", "https://example.com")
}
return nil
}
The auth function returns an erratt.Error value with username and password attributes.
The exportLogic function calls connect and handles the error:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
err := connect()
events.Stop()
return err
}
Here is the complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"github.com/SAP/xp-clifford/erratt"
)
func auth() erratt.Error {
return erratt.New("authentication failure",
"username", "test-user",
"password", "test-password",
)
}
func connect() erratt.Error {
err := auth()
if err != nil {
return erratt.Errorf("connect failed: %w", err).
With("url", "https://example.com")
}
return nil
}
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
err := connect()
events.Stop()
return err
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Running this code produces the following output:
go run ./examples/erratt/main.go export
INFO export command invoked
ERRO connect failed: authentication failure url=https://example.com username=test-user password=test-password
The error message appears on the console with all attributes displayed.
The EventHandler.Warn method handles erratt.Error values in the same manner.
Widgets
xp-clifford provides several CLI widgets to facilitate the interaction with the user.
Note that for the widgets to run, the CLI tool must be executed in an interactive terminal. This is not always the case by default, when running or debugging an application within an IDE (like GoLand) using a Run Configuration. In such cases, make sure to configure the Run Configuration appropriately. Specifically for GoLand it can be done by selecting Emulate terminal in output console.
TextInput widget
The TextInput widget prompts the user for a single line of text. Create a TextInput widget using the TextInput function from the widget package.
func TextInput(ctx context.Context, title, placeholder string, sensitive bool) (string, error)
Parameters:
- ctx: Go context for handling Ctrl-C interrupts or timeouts
- title: The prompt question displayed to the user
- placeholder: Placeholder text shown when the input is empty
- sensitive: When true, masks typed characters (useful for passwords)
The following example demonstrates an exportLogic function that prompts for a username and password:
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
username, err := widget.TextInput(ctx, "Username", "anonymous", false)
if err != nil {
return err
}
password, err := widget.TextInput(ctx, "Password", "", true)
if err != nil {
return err
}
slog.Info("data acquired",
"username", username,
"password", password,
)
events.Stop()
return err
}
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"github.com/SAP/xp-clifford/cli/widget"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
username, err := widget.TextInput(ctx, "Username", "anonymous", false)
if err != nil {
return err
}
password, err := widget.TextInput(ctx, "Password", "", true)
if err != nil {
return err
}
slog.Info("data acquired",
"username", username,
"password", password,
)
events.Stop()
return err
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
See the example in action:

MultiInput widget
The MultiInput widget creates a multi-selection interface that allows users to select multiple items from a predefined list of options:
func MultiInput(ctx context.Context, title string, options []string) ([]string, error)
Parameters:
- ctx: Go context for handling Ctrl-C interrupts or timeouts
- title: The selection prompt displayed to the user
- options: The list of selectable items
The following example demonstrates an exportLogic function that uses the MultiInput widget:
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
protocols, err := widget.MultiInput(ctx,
"Select the supported protocols",
[]string{
"FTP",
"HTTP",
"HTTPS",
"SFTP",
"SSH",
},
)
slog.Info("data acquired",
"protocols", protocols,
)
events.Stop()
return err
}
The complete source code is assembled as follows:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
"github.com/SAP/xp-clifford/cli/widget"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked")
protocols, err := widget.MultiInput(ctx,
"Select the supported protocols",
[]string{
"FTP",
"HTTP",
"HTTPS",
"SFTP",
"SSH",
},
)
slog.Info("data acquired",
"protocols", protocols,
)
events.Stop()
return err
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Running this example produces the following output:

Configuration parameters
CLI tools built using xp-clifford can be configured through several methods:
- Command-line flags
- Environment variables
- Configuration files
xp-clifford provides types and functions to facilitate configuration and management of these parameters. Configuration parameter handling is also integrated with the widget capabilities of xp-clifford.
Currently, the following configuration parameter types are supported:
boolstring[]string
All configuration parameters managed by xp-clifford implement the configparam.ConfigParam interface.
Global configuration parameters
Any CLI tool built using xp-clifford includes the following global flags:
-cor--config: Configuration file for setting additional parameters (string)-vor--verbose: Enable verbose logging (bool)-hor--help: Print help message (bool)
The verbose logging is explained in Verbose logging. The configuration file handling is elaborated in the Configuration file.
Enable verbose logging with the -v or --verbose flag. When enabled, structured log messages at the Debug level are also printed to the console.
An example exportLogic function:
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Debug("export command invoked")
events.Stop()
return nil
}
The complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Debug("export command invoked")
events.Stop()
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.SetCommand(exportLogic)
cli.Execute()
}
Executing the export subcommand without the -v flag produces no output:
go run ./examples/verbose/main.go export
With the -v flag, the debug-level message appears:
go run ./examples/verbose/main.go export -v
DEBU export command invoked
Configuration parameters of the export subcommand
The export subcommand includes the following default configuration parameters:
-kor--kind: Resource kinds to export ([]string)-oor--output: Redirect output to a file (string)
You can extend the export subcommand with additional configuration parameters using the export.AddConfigParams function:
func AddConfigParams(param ...configparam.ConfigParam)
Bool configuration parameter
Create a new bool configuration parameter using the configparam.Bool function:
func Bool(name, description string) *BoolParam
The two mandatory arguments are name and description. Fine-tune the parameter with these methods:
WithShortName: Single-character short command-line flagWithFlagName: Long format of the command-line flag (defaults to name)WithEnvVarName: Environment variable name for the parameterWithDefaultValue: Default value of the parameter
Use the Value() method to retrieve the parameter value. The IsSet() method returns true if the user has explicitly set the value.
Here is a bool configuration parameter definition:
var testParam = configparam.Bool("test", "test bool parameter").
WithShortName("t").
WithEnvVarName("CLIFFORD_TEST")
Add the parameter to the export subcommand:
export.AddConfigParams(testParam)
A complete working example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(_ context.Context, events export.EventHandler) error {
slog.Info("export command invoked", "test-value", testParam.Value())
events.Stop()
return nil
}
var testParam = configparam.Bool("test", "test bool parameter").
WithShortName("t").
WithEnvVarName("CLIFFORD_TEST")
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(testParam)
export.SetCommand(exportLogic)
cli.Execute()
}
The new parameter appears in the help output:
go run ./examples/boolparam/main.go export --help
Export test system resources and transform them into managed resources that the Crossplane provider can consume
Usage:
test-exporter export [flags]
Flags:
-h, --help help for export
-k, --kind strings Resource kinds to export
-o, --output string redirect the YAML output to a file
-t, --test test bool parameter
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
By default, test is false:
go run ./examples/boolparam/main.go export
INFO export command invoked test-value=false
Enable it using the --test flag:
go run ./examples/boolparam/main.go export --test
INFO export command invoked test-value=true
Or using the shorthand -t flag:
go run ./examples/boolparam/main.go export -t
INFO export command invoked test-value=true
Or using the CLIFFORD_TEST environment variable:
CLIFFORD_TEST=1 go run ./examples/boolparam/main.go export
INFO export command invoked test-value=true
String configuration parameter
Create a new string configuration parameter using the configparam.String function:
func String(name, description string) *StringParam
The two mandatory arguments are name and description. Fine-tune the parameter with these methods:
WithShortName: Single-character short command-line flagWithFlagName: Long format of the command-line flag (defaults to name)WithEnvVarName: Environment variable name for the parameterWithDefaultValue: Default value of the parameter
Use the Value() method to retrieve the parameter value. The IsSet() method returns true if the user has explicitly set the value.
The ValueOrAsk method returns the value if set. Otherwise, it prompts for the value interactively using the TextInput widget.
Consider the following string configuration parameter:
var testParam = configparam.String("username", "username used for authentication").
WithShortName("u").
WithEnvVarName("USERNAME").
WithDefaultValue("testuser")
A complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked",
"username", testParam.Value(),
"is-set", testParam.IsSet(),
)
// If not set, ask the value
username, err := testParam.ValueOrAsk(ctx)
if err != nil {
return err
}
slog.Info("value set by user", "value", username)
events.Stop()
return nil
}
var testParam = configparam.String("username", "username used for authentication").
WithShortName("u").
WithEnvVarName("USERNAME").
WithDefaultValue("testuser")
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(testParam)
export.SetCommand(exportLogic)
cli.Execute()
}
The new parameter appears in the help output:
go run ./examples/stringparam/main.go export --help
Export test system resources and transform them into managed resources that the Crossplane provider can consume
Usage:
test-exporter export [flags]
Flags:
-h, --help help for export
-k, --kind strings Resource kinds to export
-o, --output string redirect the YAML output to a file
-u, --username string username used for authentication
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
Set the value using the --username flag:
go run ./examples/stringparam/main.go export --username anonymous
INFO export command invoked username=anonymous is-set=true
INFO value set by user value=anonymous
Or using the shorthand -u flag:
go run ./examples/stringparam/main.go export -u anonymous
INFO export command invoked username=anonymous is-set=true
INFO value set by user value=anonymous
Or using the USERNAME environment variable:
USERNAME=anonymous go run ./examples/stringparam/main.go export
INFO export command invoked username=anonymous is-set=true
INFO value set by user value=anonymous
When no value is provided, the TextInput widget prompts for it interactively:

String slice configuration parameter
A string slice configuration parameter configures values of type []string.
Create a new string slice configuration parameter using the configparam.StringSlice function:
func StringSlice(name, description string) *StringSliceParam
The two mandatory arguments are name and description. Fine-tune the parameter with these methods:
WithShortName: Single-character short command-line flagWithFlagName: Long format of the command-line flag (defaults to name)WithEnvVarName: Environment variable name for the parameterWithDefaultValue: Default value of the parameterWithPossibleValues: Limit the selection options offered duringValueOrAskWithPossibleValuesFn: Function that provides the selection options offered duringValueOrAsk
Use the Value() method to retrieve the parameter value. The IsSet() method returns true if the user has explicitly set the value.
The ValueOrAsk method returns the value if set. Otherwise, it prompts for the value interactively using the MultiInput widget. Interactive prompting requires setting possible values with WithPossibleValues or WithPossibleValuesFn.
The following example configures a StringSlice parameter:
var testParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS")
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked",
"protocols", testParam.Value(),
"num-of-protos", len(testParam.Value()),
"is-set", testParam.IsSet(),
)
events.Stop()
return nil
}
var testParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS")
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(testParam)
export.SetCommand(exportLogic)
cli.Execute()
}
The new parameter appears in the help output:
go run ./examples/stringslice/main.go export --help
Export test system resources and transform them into managed resources that the Crossplane provider can consume
Usage:
test-exporter export [flags]
Flags:
-h, --help help for export
-k, --kind strings Resource kinds to export
-o, --output string redirect the YAML output to a file
-p, --protocol strings list of supported protocols
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
Without setting the value:
go run ./examples/stringslice/main.go export
INFO export command invoked protocols=[] num-of-protos=0 is-set=false
Set the value using the --protocol flag:
go run ./examples/stringslice/main.go export --protocol HTTP --protocol HTTPS --protocol SSH
INFO export command invoked protocols="[HTTP HTTPS SSH]" num-of-protos=3 is-set=true
Set the value using the -p flag:
go run ./examples/stringslice/main.go export -p HTTP -p SFTP -p FTP
INFO export command invoked protocols="[HTTP SFTP FTP]" num-of-protos=3 is-set=true
Set the value using the PROTOCOLS environment variable:
PROTOCOLS="HTTP HTTPS FTP" go run ./examples/stringslice/main.go export
INFO export command invoked protocols="[HTTP HTTPS FTP]" num-of-protos=3 is-set=true
To enable interactive prompting with StringSlice configuration parameters, add static selection options using the WithPossibleValues method.
Define the configuration parameter:
var testParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS").
WithPossibleValues([]string{"HTTP", "HTTPS", "FTP", "SSH", "SFTP"})
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked",
"protocols", testParam.Value(),
"num-of-protos", len(testParam.Value()),
"is-set", testParam.IsSet(),
)
protocols, err := testParam.ValueOrAsk(ctx)
if err != nil {
return err
}
slog.Info("data acquired", "protocols", protocols)
events.Stop()
return nil
}
var testParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS").
WithPossibleValues([]string{"HTTP", "HTTPS", "FTP", "SSH", "SFTP"})
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(testParam)
export.SetCommand(exportLogic)
cli.Execute()
}
You can set values with flags or environment variables as before:
go run ./examples/stringslicestatic/main.go export --protocol HTTP --protocol HTTPS --protocol SSH
INFO export command invoked protocols="[HTTP HTTPS SSH]" num-of-protos=3 is-set=true
INFO data acquired protocols="[HTTP HTTPS SSH]"
go run ./examples/stringslicestatic/main.go export -p HTTP -p SFTP -p FTP
INFO export command invoked protocols="[HTTP SFTP FTP]" num-of-protos=3 is-set=true
INFO data acquired protocols="[HTTP SFTP FTP]"
PROTOCOLS="HTTP HTTPS FTP" go run ./examples/stringslicestatic/main.go export
INFO export command invoked protocols="[HTTP HTTPS FTP]" num-of-protos=3 is-set=true
INFO data acquired protocols="[HTTP HTTPS FTP]"
When you omit the parameter values, the CLI tool prompts for them interactively:

Sometimes the set of possible StringSlice parameter values cannot be defined at build time. The value set may depend on a previous interactive selection or the result of an API request.
In such cases, set the possible values dynamically using the WithPossibleValuesFn method.
Consider a simple Bool configuration parameter:
var secureParam = configparam.Bool("secure", "secure protocol").
WithShortName("s").
WithEnvVarName("SECURE")
Based on the value of secureParam, the possibleProtocols function suggests different protocol names:
func possibleProtocols() ([]string, error) {
if secureParam.Value() {
return []string{"HTTPS", "SFTP", "SSH"}, nil
}
return []string{"FTP", "HTTP"}, nil
}
The protocolsParam configuration parameter uses possibleProtocols when prompting the user with the ValueOrAsk method:
var protocolsParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS").
WithPossibleValuesFn(possibleProtocols)
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked",
"secure", secureParam.Value(),
"secure-is-set", secureParam.IsSet(),
"protocols", protocolsParam.Value(),
"num-of-protos", len(protocolsParam.Value()),
"protocols-is-set", protocolsParam.IsSet(),
)
protocols, err := protocolsParam.ValueOrAsk(ctx)
if err != nil {
return err
}
slog.Info("data acquired", "protocols", protocols)
events.Stop()
return nil
}
func possibleProtocols() ([]string, error) {
if secureParam.Value() {
return []string{"HTTPS", "SFTP", "SSH"}, nil
}
return []string{"FTP", "HTTP"}, nil
}
var secureParam = configparam.Bool("secure", "secure protocol").
WithShortName("s").
WithEnvVarName("SECURE")
var protocolsParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS").
WithPossibleValuesFn(possibleProtocols)
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(secureParam, protocolsParam)
export.SetCommand(exportLogic)
cli.Execute()
}
Both parameters appear in the help output:
go run ./examples/stringslicedynamic/main.go export --help
Export test system resources and transform them into managed resources that the Crossplane provider can consume
Usage:
test-exporter export [flags]
Flags:
-h, --help help for export
-k, --kind strings Resource kinds to export
-o, --output string redirect the YAML output to a file
-p, --protocol strings list of supported protocols
-s, --secure secure protocol
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
Set the values using flags as usual:
go run ./examples/stringslicedynamic/main.go export -s --protocol HTTPS --protocol SFTP
INFO export command invoked secure=true secure-is-set=true protocols="[HTTPS SFTP]" num-of-protos=2 protocols-is-set=true
INFO data acquired protocols="[HTTPS SFTP]"
When the protocol configuration parameter is not set, the CLI prompts for its value interactively. The available options depend on the value of secure.
If secure is not set:

If secure is set:

Subcommands
CLI tools created with xp-clifford include the mandatory export subcommand. You can also define additional subcommands by creating a value that implements the cli.SubCommand interface.
You can implement your own type, or use the cli.BasicSubCommand type, which already implements the cli.SubCommand interface.
The business logic executed when the subcommand is invoked must have the following function signature:
func(context.Context) error
Let's consider the following logic function for an imaginary login subcommand:
func login(_ context.Context) error {
slog.Info("login invoked")
return nil
}
A BasicSubcommand value can be created for the login subcommand:
var loginSubCommand = &cli.BasicSubCommand{
Name: "login",
Short: "Login demo subcommand",
Long: "A subcommand demonstrating xp-clifford capabilities",
ConfigParams: []configparam.ConfigParam{},
Run: login,
}
A subcommand can be registered using the cli.RegisterCommand function:
cli.RegisterSubCommand(loginSubCommand)
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
_ "github.com/SAP/xp-clifford/cli/export"
)
func login(_ context.Context) error {
slog.Info("login invoked")
return nil
}
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
var loginSubCommand = &cli.BasicSubCommand{
Name: "login",
Short: "Login demo subcommand",
Long: "A subcommand demonstrating xp-clifford capabilities",
ConfigParams: []configparam.ConfigParam{},
Run: login,
}
cli.RegisterSubCommand(loginSubCommand)
cli.Execute()
}
The login subcommand appears when we run the CLI application with the --help flag:
go run ./examples/loginsubcommand/main.go --help
test system exporting tool is a CLI tool for exporting existing resources as Crossplane managed resources
Usage:
test-exporter [command]
Available Commands:
completion Generate the autocompletion script for the specified shell
export Export test system resources
help Help about any command
login Login demo subcommand
Flags:
-c, --config string Configuration file
-h, --help help for test-exporter
-v, --verbose Verbose output
Use "test-exporter [command] --help" for more information about a command.
The --help flag also works for the new login subcommand:
go run ./examples/loginsubcommand/main.go login --help
A subcommand demonstrating xp-clifford capabilities
Usage:
test-exporter login [flags]
Flags:
-h, --help help for login
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
We can also run the login subcommand:
go run ./examples/loginsubcommand/main.go login
INFO login invoked
Custom subcommands can be extended with configuration parameters using the GetConfigParams() method of the cli.SubCommand interface, or by setting the ConfigParams field of a BasicSubCommand value.
Let's update the loginSubCommand value:
var loginSubCommand = &cli.BasicSubCommand{
Name: "login",
Short: "Login demo subcommand",
Long: "A subcommand demonstrating xp-clifford capabilities",
ConfigParams: []configparam.ConfigParam{
testParam,
},
Run: login,
}
Here, testParam is defined as follows:
var testParam = configparam.Bool("test", "test bool parameter").
WithShortName("t").
WithEnvVarName("CLIFFORD_TEST")
Let's extend the login function to print the value of testParam:
func login(_ context.Context) error {
slog.Info("login invoked", "test", testParam.Value())
return nil
}
Complete example:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
_ "github.com/SAP/xp-clifford/cli/export"
)
func login(_ context.Context) error {
slog.Info("login invoked", "test", testParam.Value())
return nil
}
var testParam = configparam.Bool("test", "test bool parameter").
WithShortName("t").
WithEnvVarName("CLIFFORD_TEST")
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
var loginSubCommand = &cli.BasicSubCommand{
Name: "login",
Short: "Login demo subcommand",
Long: "A subcommand demonstrating xp-clifford capabilities",
ConfigParams: []configparam.ConfigParam{
testParam,
},
Run: login,
}
cli.RegisterSubCommand(loginSubCommand)
cli.Execute()
}
The --help flag for the login subcommand now shows the -t / --test parameter:
go run ./examples/loginsubcommandparam/main.go login --help
A subcommand demonstrating xp-clifford capabilities
Usage:
test-exporter login [flags]
Flags:
-h, --help help for login
-t, --test test bool parameter
Global Flags:
-c, --config string Configuration file
-v, --verbose Verbose output
Let's invoke the login command:
go run ./examples/loginsubcommandparam/main.go login
INFO login invoked test=false
Let's see the configuration parameter in action:
go run ./examples/loginsubcommandparam/main.go login -t
INFO login invoked test=true
Configuration file
In addition to CLI flags and environment variables, a CLI tool built with xp-clifford can read configuration from a YAML file.
You can specify the configuration file path using the --config / -c global flag.
If you don't specify a configuration file, the CLI looks for one in these locations, in order:
$XDG_CONFIG_HOME/<config_file_name>$HOME/<config_file_name>
The config_file_name is export-cli-config-<shortname>, where shortname is the value of cli.Configuration.ShortName.
The YAML file contains key-value pairs, where keys are configuration parameter names in lowercase.
Here is a simple example CLI with three configuration parameters:
package main
import (
"context"
"log/slog"
"github.com/SAP/xp-clifford/cli"
"github.com/SAP/xp-clifford/cli/configparam"
"github.com/SAP/xp-clifford/cli/export"
)
func exportLogic(ctx context.Context, events export.EventHandler) error {
slog.Info("export command invoked",
"protocols", protocolParam.Value(),
"username", usernameParam.Value(),
"boolparam", boolParam.Value(),
)
events.Stop()
return nil
}
var protocolParam = configparam.StringSlice("protocol", "list of supported protocols").
WithShortName("p").
WithEnvVarName("PROTOCOLS")
var usernameParam = configparam.String("username", "username used for authentication").
WithShortName("u").
WithEnvVarName("USERNAME")
var boolParam = configparam.Bool("bool", "test bool parameter").
WithShortName("b").
WithEnvVarName("CLIFFORD_BOOL")
func main() {
cli.Configuration.ShortName = "test"
cli.Configuration.ObservedSystem = "test system"
export.AddConfigParams(protocolParam, usernameParam, boolParam)
export.SetCommand(exportLogic)
cli.Execute()
}
Flag-based configuration works as expected:
go run ./examples/configfile/main.go export -b --protocol HTTPS --protocol SFTP --username anonymous
INFO export command invoked protocols="[HTTPS SFTP]" username=anonymous boolparam=true
Without CLI flags:
go run ./examples/configfile/main.go export
INFO export command invoked protocols=[] username="" boolparam=false
Now let's create a configuration file:
protocol:
- HTTP
- FTP
username: config-user
bool: true
The CLI reads configuration parameter values from this file:
go run ./examples/configfile/main.go export --config ./examples/configfile/config
INFO export command invoked protocols="[HTTP FTP]" username=config-user boolparam=true
Environment variables override values from the configuration file:
PROTOCOLS="FTP" go run ./examples/configfile/main.go export --config ./examples/configfile/config
INFO export command invoked protocols=[FTP] username=config-user boolparam=true
CLI flags take the highest precedence and override everything else:
PROTOCOLS="FTP" go run ./examples/configfile/main.go export --config ./examples/configfile/config --protocol SSH -b=false
INFO export command invoked protocols=[SSH] username=config-user boolparam=false
Parsing and sanitizing
When creating Crossplane managed resource definitions, we frequently transform objects describing external resources into a different schema. Usually the values are preserved, but the data structure differs.
Sometimes we cannot preserve values exactly because they must conform to certain rules.
An example is the metadata.name field of Kubernetes resources1. The Kubernetes documentation references various RFCs and extends those requirements with additional rules.
The parsan package in xp-clifford provides functions that transform strings into formats satisfying different Kubernetes object name requirements. This process is called sanitization. The ParseAndSanitize function performs this action:
func ParseAndSanitize(input string, rule Rule) []string
The ParseAndSanitize function takes an input string and a rule, then transforms the input to conform to the rule. Since multiple valid sanitized solutions may exist, the function returns all of them.
Sanitizer rules
The following rules are available for sanitization.
The RFC1035Subdomain rule conforms to:
<subdomain> ::= <label> | <subdomain> "." <label>
A subdomain is either a single label or multiple labels separated by dots (e.g., label.label.label).
A label is a string that:
- starts with a letter (lowercase or uppercase),
- ends with a letter (lowercase or uppercase) or a digit,
- contains only letters, digits, and
-characters.
A label cannot exceed 63 characters. A subdomain cannot exceed 253 characters.
During sanitization, invalid characters are replaced with - or x. The @ symbol is replaced with -at-. Labels and subdomains that are too long are trimmed.
Examples:
| input | sanitized |
|---|---|
www.example.com |
www.example.com |
Can you sanitize me? |
Can-you-sanitize-mex |
99Luftballons |
x99Luftballons |
admin@example.com |
admin-at-example.com |
The RFC1035LowerSubdomain rule is a variation of RFC1035Subdomain that requires lowercase letters only. Uppercase letters are converted to lowercase:
| input | sanitized |
|---|---|
www.example.com |
www.example.com |
Can you sanitize me? |
can-you-sanitize-mex |
99Luftballons |
x99luftballons |
admin@example.com |
admin-at-example.com |
The RFC1035SubdomainRelaxed rule is a variation of RFC1035Subdomain that allows labels to start with digits:
| input | sanitized |
|---|---|
www.example.com |
www.example.com |
Can you sanitize me? |
Can-you-sanitize-mex |
99Luftballons |
99Luftballons |
admin@example.com |
admin-at-example.com |
The RFC1035LowerSubdomainRelaxed rule combines RFC1035LowerSubdomain and RFC1035SubdomainRelaxed. Uppercase characters are converted to lowercase, and labels may start with digits:
| input | sanitized |
|---|---|
www.example.com |
www.example.com |
Can you sanitize me? |
can-you-sanitize-mex |
99Luftballons |
99luftballons |
admin@example.com |
admin-at-example.com |
Packages
mkcontainer
The mkcontainer package provides a thread-safe multi-key container for storing and retrieving items indexed by multiple keys simultaneously.
Features
- Multi-key indexing: Items can be indexed by GUID, name, or both
- Thread-safe: All operations are safe for concurrent use
- Flexible storage: Store any type that implements the required interfaces
- Efficient lookups: O(1) lookups by GUID, O(1) lookups by name
Interfaces
Items are indexed based on which interfaces they implement:
| Interface | Method | Uniqueness | Lookup Returns |
|---|---|---|---|
ItemWithGUID |
GetGUID() string |
Must be unique | Single item |
ItemWithName |
GetName() string |
Not unique | Slice of items |
An item may implement both interfaces to be indexed by both GUID and name.
Example
package main
import (
"fmt"
"github.com/SAP/xp-clifford/mkcontainer"
)
// Document implements both ItemWithGUID and ItemWithName
type Document struct {
ID string
Title string
}
func (d *Document) GetGUID() string { return d.ID }
func (d *Document) GetName() string { return d.Title }
func main() {
c := mkcontainer.New()
// Store multiple documents
c.Store(
&Document{ID: "doc-1", Title: "Report"},
&Document{ID: "doc-2", Title: "Report"},
&Document{ID: "doc-3", Title: "Summary"},
)
// Lookup by unique GUID
doc := c.GetByGUID("doc-1")
fmt.Printf("Found: %s\n", doc.(*Document).Title)
// Lookup all documents with the same name
reports := c.GetByName("Report")
fmt.Printf("Found %d reports\n", len(reports))
// Iterate over all GUIDs (sorted)
for _, guid := range c.GetGUIDs() {
fmt.Printf("GUID: %s\n", guid)
}
// Iterate over all items by GUID
for guid, item := range c.AllByGUIDs() {
fmt.Printf("%s -> %s\n", guid, item.(*Document).Title)
}
}
Footnotes
Directories
¶
| Path | Synopsis |
|---|---|
|
configparam
Package configparam defines the configuration parameters that a CLI tool can use.
|
Package configparam defines the configuration parameters that a CLI tool can use. |
|
export
Package export defines the export subcommand.
|
Package export defines the export subcommand. |
|
widget
Package widget supplies interactive CLI widgets.
|
Package widget supplies interactive CLI widgets. |
|
Package erratt provides enhanced Go errors with attributes and wrapping.
|
Package erratt provides enhanced Go errors with attributes and wrapping. |
|
examples
|
|
|
basic
command
This example demonstrates the simplest possible CLI implementation using the export CLI framework.
|
This example demonstrates the simplest possible CLI implementation using the export CLI framework. |
|
boolparam
command
|
|
|
configfile
command
|
|
|
configparams
command
This example demonstrates a CLI implementation with a subcommand using the export CLI framework.
|
This example demonstrates a CLI implementation with a subcommand using the export CLI framework. |
|
erratt
command
|
|
|
export
command
This example demonstrates a CLI implementation with a basic export subcommand.
|
This example demonstrates a CLI implementation with a basic export subcommand. |
|
exportcomment
command
|
|
|
exportresource
command
This example demonstrates a CLI implementation with the `export` subcommand that generates test resource definitions when invoked.
|
This example demonstrates a CLI implementation with the `export` subcommand that generates test resource definitions when invoked. |
|
exportsingle
command
|
|
|
exportwarn
command
|
|
|
loginsubcommand
command
|
|
|
loginsubcommandparam
command
|
|
|
multiinput
command
|
|
|
parsan
command
|
|
|
stringparam
command
|
|
|
stringslice
command
|
|
|
stringslicedynamic
command
|
|
|
stringslicestatic
command
|
|
|
subcommand
command
This example demonstrates a CLI implementation with a subcommand and command line flags using the export CLI framework.
|
This example demonstrates a CLI implementation with a subcommand and command line flags using the export CLI framework. |
|
textinput
command
|
|
|
verbose
command
|
|
|
Package mkcontainer provides a thread-safe multi-key container for storing and retrieving items indexed by multiple keys simultaneously.
|
Package mkcontainer provides a thread-safe multi-key container for storing and retrieving items indexed by multiple keys simultaneously. |
|
Package parsan is a string parser and sanitizer function.
|
Package parsan is a string parser and sanitizer function. |
|
Package yaml converts Kubernetes resources to YAML strings.
|
Package yaml converts Kubernetes resources to YAML strings. |