diff --git a/docs/sources/k6/next/extensions/_index.md b/docs/sources/k6/next/extensions/_index.md index 618637e1a4..5dace6d317 100644 --- a/docs/sources/k6/next/extensions/_index.md +++ b/docs/sources/k6/next/extensions/_index.md @@ -8,8 +8,9 @@ weight: 700 With k6 extensions, you can extend the core k6 functionality with new features to support your specific reliability-testing needs. -k6 supports three types of extensions: +k6 supports four types of extensions: - **JavaScript extensions** extend the JavaScript APIs available to your test scripts. They can add support for new network protocols, improve performance compared to equivalent JS libraries, or add features. - **Output extensions** send metrics to a custom file format or service. They can add custom processing and dispatching methods. - **Secret Source extensions** provide secrets to your tests. +- **Subcommand extensions** add custom CLI commands under the `k6 x` namespace for setup, validation, and integration tools. diff --git a/docs/sources/k6/next/extensions/create/_index.md b/docs/sources/k6/next/extensions/create/_index.md index c608aed65b..a0e6cf3bba 100644 --- a/docs/sources/k6/next/extensions/create/_index.md +++ b/docs/sources/k6/next/extensions/create/_index.md @@ -13,6 +13,7 @@ consider building your own extension. Use the tutorials below to get started. - [Create a JavaScript extension](https://grafana.com/docs/k6//extensions/create/javascript-extensions) to extend the JavaScript functionality of your script or add support for a new network protocol to test. - [Create an Output extension](https://grafana.com/docs/k6//extensions/create/output-extensions) to process the metrics emitted by k6 or publish them to unsupported backend stores. - [Create a Secret Source extension](https://grafana.com/docs/k6//extensions/create/secret-source_extensions) to provide secrets to your k6 script at runtime. +- [Create a Subcommand extension](https://grafana.com/docs/k6//extensions/create/subcommand-extensions) to add custom CLI commands under the `k6 x` namespace. ## Necessary knowledge diff --git a/docs/sources/k6/next/extensions/create/subcommand-extensions.md b/docs/sources/k6/next/extensions/create/subcommand-extensions.md new file mode 100644 index 0000000000..9ad1ff1b4c --- /dev/null +++ b/docs/sources/k6/next/extensions/create/subcommand-extensions.md @@ -0,0 +1,238 @@ +--- +title: 'Subcommand extensions' +description: 'Follow these steps to build a subcommand extension for k6.' +weight: 600 +--- + +# Subcommand extensions + +k6 provides a rich set of built-in commands, but some use cases require custom CLI tools that integrate with k6's runtime and state. Subcommand extensions allow you to register custom commands under the `k6 x` namespace, providing a standardized way to extend k6's CLI functionality. + +Subcommand extensions are useful for: + +- Setup and configuration tools (for example, verifying system requirements) +- Custom validation and testing utilities +- Integration tools that interact with k6's runtime state +- Helper commands specific to your testing infrastructure + +Like other k6 extensions, subcommand extensions are built as Go modules that implement specific APIs and are compiled into custom k6 binaries using xk6. + +## Before you start + +To run this tutorial, you'll need the following applications installed: + +- Go +- Git + +You also need to install xk6: + +```bash +go install go.k6.io/xk6/cmd/xk6@latest +``` + +## Write a simple extension + +1. Set up a directory to work in. + +```bash +mkdir xk6-subcommand-mytool; cd xk6-subcommand-mytool; go mod init xk6-subcommand-mytool +``` + +2. The core of a subcommand extension is a constructor function that creates a Cobra command. The constructor receives k6's `GlobalState` for read-only access to runtime configuration. + +Create a simple example command: + +```go +package mytool + +import ( + "github.com/spf13/cobra" + "go.k6.io/k6/cmd/state" + "go.k6.io/k6/subcommand" +) + +func init() { + subcommand.RegisterExtension("mytool", newCommand) +} + +func newCommand(gs *state.GlobalState) *cobra.Command { + return &cobra.Command{ + Use: "mytool", + Short: "My custom tool", + Long: "A custom tool that integrates with k6", + Run: func(cmd *cobra.Command, args []string) { + gs.Logger.Info("Running mytool") + // Custom logic here + }, + } +} +``` + +3. The extension uses the `subcommand.RegisterExtension` function to register itself during initialization. The first argument is the command name (which must match the command's `Use` field), and the second is the constructor function. + +{{< admonition type="caution" >}} + +The `GlobalState` provided to your command is read-only. Do not modify it, as this can cause core k6 instability. + +{{< /admonition >}} + +## Build k6 with the extension + +Once you've written your extension, build a custom k6 binary with it: + +```bash +xk6 build --with xk6-subcommand-mytool=. +``` + +This creates a `k6` binary in your current directory that includes your extension. + +## Use the extension + +After building, your subcommand is available under the `k6 x` namespace: + +```bash +./k6 x mytool +``` + +To see all available extension subcommands: + +```bash +./k6 x help +``` + +## Constructor requirements + +The constructor function passed to `RegisterExtension` must: + +1. Accept a single `*state.GlobalState` parameter +2. Return a `*cobra.Command` +3. Create a command whose `Use` field matches the registered extension name + +Violating these requirements causes the extension to panic at startup, ensuring configuration errors are caught early. + +## Example: Complete validation tool + +Here's a more complete example that checks system requirements: + +```go +package validate + +import ( + "fmt" + "os" + "runtime" + + "github.com/spf13/cobra" + "go.k6.io/k6/cmd/state" + "go.k6.io/k6/subcommand" +) + +func init() { + subcommand.RegisterExtension("validate", newValidateCommand) +} + +func newValidateCommand(gs *state.GlobalState) *cobra.Command { + cmd := &cobra.Command{ + Use: "validate", + Short: "Verify system requirements", + Long: "Check if the system meets requirements for running tests", + RunE: func(cmd *cobra.Command, args []string) error { + gs.Logger.Info("Checking system requirements...") + + // Check Go version + gs.Logger.Infof("Go version: %s", runtime.Version()) + + // Check available memory + var m runtime.MemStats + runtime.ReadMemStats(&m) + gs.Logger.Infof("Available memory: %d MB", m.Sys/1024/1024) + + // Check environment variables + if broker := os.Getenv("MQTT_BROKER"); broker != "" { + gs.Logger.Infof("MQTT broker configured: %s", broker) + } else { + gs.Logger.Warn("MQTT_BROKER not set") + } + + gs.Logger.Info("Validation complete") + return nil + }, + } + + return cmd +} +``` + +Usage: + +```bash +./k6 x validate +``` + +## Access k6 runtime state + +The `GlobalState` provides read-only access to k6's configuration: + +```go +func newCommand(gs *state.GlobalState) *cobra.Command { + return &cobra.Command{ + Use: "validate", + Short: "Validate k6 configuration", + Run: func(cmd *cobra.Command, args []string) { + // Access logger + gs.Logger.Info("Validating k6 configuration...") + + // Access flags and options + if gs.Flags.Verbose { + gs.Logger.Debug("Verbose mode enabled") + } + + // Access environment variables + gs.Logger.Infof("Working directory: %s", gs.Getwd) + }, + } +} +``` + +## Add command flags + +Use Cobra's flag system to add options to your subcommand: + +```go +func newCommand(gs *state.GlobalState) *cobra.Command { + var target string + var verbose bool + + cmd := &cobra.Command{ + Use: "validate", + Short: "Validate configuration", + Run: func(cmd *cobra.Command, args []string) { + if verbose { + gs.Logger.Info("Verbose mode enabled") + } + gs.Logger.Infof("Validating target: %s", target) + // Validation logic here + }, + } + + cmd.Flags().StringVarP(&target, "target", "t", "localhost", "Target to validate") + cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output") + + return cmd +} +``` + +Usage: + +```bash +./k6 x validate --target example.com --verbose +``` + +## Best practices + +- **Read-only state**: Never modify the `GlobalState` passed to your constructor +- **Naming**: Use descriptive, kebab-case names for your commands +- **Documentation**: Provide clear `Short` and `Long` descriptions +- **Error handling**: Return errors from `RunE` rather than panicking in command execution +- **Logging**: Use `gs.Logger` for consistent output with k6's logging system +