Skip to content

Latest commit

 

History

History
290 lines (195 loc) · 9.65 KB

CONTRIBUTING.md

File metadata and controls

290 lines (195 loc) · 9.65 KB

Contributing

Thanks for your interest in contributing to the Confluent CLI!

Development Environment

Start by following these steps to set up your computer for CLI development:

Go Version

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

Developing on MacOS

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

Security

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

File Layout

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)

Testing

The CLI is tested with a combination of unit tests and integration tests. To run all tests:

make test

Unit Tests

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"

Integration Tests

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:

  1. Run a test HTTP server to mock Confluent Cloud or the Confluent Platform Control Plane API.
  2. Run a logical sequence of CLI commands.
  3. 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"

Example: Adding a New Command to the CLI

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

Creating the Command

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
}

Registering the Command

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))

Running the Command

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

Integration Testing

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:

  1. Copied directly from the terminal.
  2. 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 Autocompletion

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.

Adding a New Delete Command

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.

Opening a PR

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".

Detailed Implementation Guides

Please familiarize yourself with the following resources before writing your first CLI command: