Thanks for your interest in contributing to the Confluent CLI!
Start by following these steps to set up your computer for CLI development:
We recommend you use goenv to manage your Go versions.
There's a .go-version
file in this repo with the exact version we use (and test against in CI).
We recommend cloning the goenv
repo directly to ensure that you have access to the latest version of Go. If you've
already installed goenv
with brew, uninstall it first:
brew uninstall goenv
Now, clone the goenv
repo:
git clone https://github.com/syndbg/goenv.git ~/.goenv
Then, add the following to your shell profile:
export GOENV_ROOT="$HOME/.goenv"
export PATH="$GOENV_ROOT/bin:$PATH"
eval "$(goenv init -)"
export PATH="$PATH:$GOPATH/bin"
Finally, you can install the appropriate version of Go by running the following command inside the root directory of the repository:
goenv install
Our integration tests read a lot of files while they are running. On MacOS, the default maximum number of open files is
256, which is too small (you will see an error like error retrieving command exit code
or too many open files
).
Please run the following three commands and then restart for these changes to take effect:
echo 'kern.maxfiles=20480' | sudo tee -a /etc/sysctl.conf
echo -e 'limit maxfiles 8192 20480\nlimit maxproc 1000 2000' | sudo tee -a /etc/launchd.conf
echo 'ulimit -n 4096' | sudo tee -a /etc/profile
We use pre-commit
hooks and gitleaks
to prevent secrets from being committed to this repo. Please install pre-commit
hooks (Note that the second command should be run inside the root directory of the repository):
brew install pre-commit
pre-commit install
This repo mostly follows the Standard Go Project Layout. Here's the basic file structure:
cli/
├─ cmd/
│ ├─ confluent/
│ │ ├─ main.go (entry point for the CLI binary)
├─ dist/
│ ├─ confluent_<os>_<arch>/
│ │ ├─ confluent (the CLI binary)
├─ internal/
│ ├─ cmd/ (CLI commands)
│ │ ├─ <command>/
│ │ │ ├─ command.go (a top-level CLI command)
│ │ │ ├─ command_<subcommand>.go (a subcommand of a top-level CLI command)
│ │ │ ├─ command_<subcommand>_onprem.go (the on-prem version of the above command, if applicable)
│ │ ├─ command.go (the root CLI command)
│ ├─ pkg/
├─ test/ (integration tests)
│ ├─ fixtures/
│ │ ├─ output/
│ │ │ ├─ <command>/ (the golden files for a top-level CLI command)
│ │ ├─ cli_test.go (entry point for all integration tests)
│ │ ├─ <command>_test.go (the integration tests for a top-level CLI command)
The CLI is tested with a combination of unit tests and integration tests. To run all tests:
make test
Unit tests exist in files ending with _test.go
, and are located alongside the main source code files.
Unit tests should test small, isolated functions, and should not be unnecessarily complex (i.e. mocking backend calls or CLI commands).
To run the all unit tests:
make unit-test
To run a subset of unit tests, you must specify the suite and optionally the name of a specific tests:
# Run a suite of unit tests
make unit-test UNIT_TEST_ARGS="-run TestApiTestSuite"
# Run a specific unit test within a suite
make unit-test UNIT_TEST_ARGS="-run TestApiTestSuite/TestCreateCloudAPIKey"
The test/ directory contains our integration tests. These tests build the CLI binary and invoke commands on it. These CLI integration tests roughly follow this pattern:
- Run a test HTTP server to mock Confluent Cloud or the Confluent Platform Control Plane API.
- Run a logical sequence of CLI commands.
- Ensure that the output of these commands matches the corresponding golden files.
To update the golden files from the current output:
make integration-test INTEGRATION_TEST_ARGS="-update"
To skip rebuilding the CLI, if it already exists in dist/
:
make integration-test INTEGRATION_TEST_ARGS="-no-rebuild"
To run a subset of integration tests, you must specify the suite and optionally the name of a specific test:
# Run a suite of integration tests
make integration-test INTEGRATION_TEST_ARGS="-run TestCLI/TestKafka"
# Run a specific integration test within a suite
make integration-test INTEGRATION_TEST_ARGS="-run TestCLI/TestKafka/kafka_cluster_--help"
As a basic demonstration, we'll be implementing a command which prints the name of the CLI config file a specified number of times:
$ confluent config describe 3
~/.confluent/config.json
~/.confluent/config.json
~/.confluent/config.json
Like all other commands, this command will reside in internal
. First, we must create a directory for this command:
mkdir internal/config
Next, we create two files, one for the top-level command config
, and another for describe
.
internal/config/command.go
:
package config
import (
"github.com/spf13/cobra"
pcmd "github.com/confluentinc/cli/v4/pkg/cmd"
)
type command struct {
*pcmd.CLICommand
}
func New(prerunner pcmd.PreRunner) *cobra.Command {
cmd := &cobra.Command{Use: "config"}
c := &command{pcmd.NewAnonymousCLICommand(cmd, prerunner)}
cmd.AddCommand(c.newDescribeCommand())
return cmd
}
internal/config/command_describe.go
:
package config
import (
"strconv"
"github.com/spf13/cobra"
"github.com/confluentinc/cli/v4/pkg/errors"
"github.com/confluentinc/cli/v4/pkg/utils"
)
func (c *command) newDescribeCommand() *cobra.Command {
return &cobra.Command{
Use: "describe",
Args: cobra.ExactArgs(1),
RunE: c.describe,
}
}
func (c *command) describe(_ *cobra.Command, args []string) error {
filename := c.CLICommand.Config.Config.Filename
if filename == "" {
return fmt.Errorf("config file not found")
}
n, err := strconv.Atoi(args[0])
if err != nil {
return err
}
for i := 0; i < n; i++ {
output.Println(filename)
}
return nil
}
Finally, we must add the newly created config
command as a child of the root command.
Add the following line to internal/command.go
, and make sure to import its package:
cmd.AddCommand(config.New(prerunner))
To build the CLI binary, run make build
. After this, we can run our command in the following way, and see that it (hopefully) works!
make build
dist/confluent_<os>_<arch>/confluent config file describe 3
There's not much code here to unit test, so we'll skip right to integration testing. Create the following file:
test/config/config_test.go
:
package test
func (s *CLITestSuite) TestConfigDescribe() {
tests := []CLITest{
{args: "config describe 3", fixture: "config/1.golden"},
}
for _, test := range tests {
s.runConfluentTest(test)
}
}
We'll also need to add the new golden file, test/fixtures/output/config/1.golden
.
After running the command manually to ensure the output is correct, the content for the golden file can either be:
- Copied directly from the terminal.
- Updated automatically with
make integration-test INTEGRATION_TEST_ARGS="-run TestCLI/TestConfigDescribe -update"
(slow).
Now, run make integration-test INTEGRATION_TEST_ARGS="-run TestCLI/TestConfigDescribe"
and verify that it works!
Add support for autocompletion using ValidArgsFunction
if applicable (for example, if the command takes resource IDs or resource names as arguments):
func (c *command) newDescribeCommand() *cobra.Command {
return &cobra.Command{
Use: "describe",
Args: cobra.ExactArgs(1),
ValidArgsFunction: pcmd.NewValidArgsFunction(c.validArgs),
RunE: c.describe,
}
}
See the Autocompletion resource for implementation details.
For most resource types, a delete
command should support multiple arguments. The exceptions are resources which do not have an ID (e.g. ACLs, role bindings) or unique resources (e.g. the Schema Registry cluster).
See Supporting Multiple Deletion for instructions on how to write such commands.
That's it! As you can see, the process of adding a new CLI command is pretty straightforward. You can open a PR if:
- You're able to build the CLI with
make build
. - All unit and integration test pass with
make test
. - Running
make lint
produces no linter errors.
Note: If there is a JIRA ticket associated with your PR, please format the PR description as "[CLI-1234] Description of PR".
Please familiarize yourself with the following resources before writing your first CLI command: