Go CLI for Qdrant Cloud, built with Cobra / Viper and gRPC.
- Module:
github.com/qdrant/qcloud-cli - Binary:
qcloud(built tobuild/qcloud) - Go version: 1.26+
- Key dependencies:
cobra,viper,google.golang.org/grpc,qdrant-cloud-public-api(generated gRPC stubs)
cmd/qcloud/ # main entrypoint — creates State, builds root command, runs it
internal/
cli/ # root cobra command, global flags, subcommand registration
cmd/ # one sub-package per top-level subcommand
cluster/ # cluster.go (parent) + list/describe/create/delete
version/ # version subcommand
clusterutil/ # shared cluster helpers (e.g. wait-for-healthy)
output/ # shared output formatting helpers
util/ # shared command utilities
qcloudapi/ # gRPC client wrapper for the Qdrant Cloud API
state/ # State struct (shared deps: config, lazy gRPC client)
config/ # Viper-based config (file, env vars, flags
Always use Makefile targets — never raw go build, go test, or linter commands.
| Target | What it does |
|---|---|
make build |
Compile binary to build/qcloud |
make test |
Run all tests |
make lint |
Run golangci-lint (installs it if missing) |
make format |
Run golangci-lint with --fix |
make bootstrap |
Install tool dependencies via mise install |
make clean |
Remove build artifacts |
To verify your changes, you should run the following makefile targets:
make lint
make build
make testIf make lint fails from formatting problems, use make format to fix them.
Every leaf command and group command must have a Long description and an Example block.
Long:
- First line expands the
Shortdescription into a full sentence. - Blank line, then one or two paragraphs explaining behaviour, use cases, and important caveats.
- Use the proto service/message comments as the authoritative source of truth for what a resource or operation does.
- Do NOT describe individual flags — only document unusual or non-obvious flag interactions.
Example:
- One example per meaningful use case (basic call, common flag combinations, scripting).
- Prefix every line with
#comment explaining what the example does. - Real command invocations with plausible IDs/values.
All five base types (ListCmd, DescribeCmd, CreateCmd, UpdateCmd, Cmd) expose Long and Example as top-level struct fields. Never set them inside BaseCobraCommand().
Every new command package must ship tests. This is not optional.
- Place tests in
internal/cmd/<group>/as<file>_test.gousingpackage <group>_test. - Use
testutil.NewTestEnv+testutil.Exec— never call command functions directly. - When adding a new gRPC service, also add a
fake_<service>.goininternal/testutil/and register it inserver.go/TestEnv. - Cover: table output (assert header columns + key values), JSON output (unmarshal and assert), request fields sent to server, backend errors (use
Returns(nil, fmt.Errorf(...))and assertrequire.Error), input errors (missing args, wrong flags). - Run
make testbefore declaring done.
Each subcommand group lives in internal/cmd/<group>/:
- A public
NewCommand(s *state.State) *cobra.Commandcreates the parent and registers sub-commands. - Leaf commands are unexported (
newListCommand,newDeleteCommand, …). - All commands receive
*state.State— use it to access config and the lazy gRPC client.
main → state.New(version) → passed to every command constructor. Commands call s.Client(ctx) to get the gRPC client (created on first use) and s.AccountID() for the current account.
All leaf commands are built using one of five generic base types. Always prefer these over raw cobra.Command.
For listing resources. OutputTable must be set. The base automatically registers --no-headers and handles header suppression. By default the command takes no positional args; set Args to accept them.
base.ListCmd[*foov1.ListFoosResponse]{
Use: "list",
Short: "List all foos",
Fetch: func(s *state.State, cmd *cobra.Command) (*foov1.ListFoosResponse, error) {
// call gRPC, return response
},
OutputTable: func(_ *cobra.Command, w io.Writer, resp *foov1.ListFoosResponse) (output.TableRenderer, error) {
t := output.NewTable[*foov1.Foo](w)
t.AddField("ID", func(v *foov1.Foo) string { return v.GetId() })
t.SetItems(resp.GetItems())
return t, nil
},
}.CobraCommand(s)For fetching and displaying a single resource (typically by ID positional arg).
base.DescribeCmd[*foov1.Foo]{
Use: "describe <foo-id>",
Short: "Describe a foo",
Args: util.ExactArgs(1, "a foo ID"),
Fetch: func(s *state.State, cmd *cobra.Command, args []string) (*foov1.Foo, error) {
// call gRPC using args[0]
},
PrintText: func(_ *cobra.Command, w io.Writer, resource *foov1.Foo) error {
// print fields
return nil
},
}.CobraCommand(s)For creating a resource. Define flags in BaseCobraCommand; read them in Run via cmd.Flags().GetString() — do NOT use bound vars.
base.CreateCmd[*foov1.Foo]{
BaseCobraCommand: func() *cobra.Command {
cmd := &cobra.Command{Use: "create", Short: "Create a foo", Args: cobra.NoArgs}
cmd.Flags().String("name", "", "Name of the foo")
return cmd
},
Run: func(s *state.State, cmd *cobra.Command, args []string) (*foov1.Foo, error) {
name, _ := cmd.Flags().GetString("name")
// call gRPC, return created resource
},
PrintResource: func(_ *cobra.Command, out io.Writer, resource *foov1.Foo) {
fmt.Fprintf(out, "Foo %s created.\n", resource.GetId())
},
}.CobraCommand(s)For updating a resource. Fetches first, then applies changes.
base.UpdateCmd[*foov1.Foo]{
BaseCobraCommand: func() *cobra.Command {
cmd := &cobra.Command{Use: "update <foo-id>", Short: "Update a foo", Args: cobra.ExactArgs(1)}
cmd.Flags().String("name", "", "New name")
return cmd
},
Fetch: func(s *state.State, cmd *cobra.Command, args []string) (*foov1.Foo, error) {
// fetch existing resource by args[0]
},
Update: func(s *state.State, cmd *cobra.Command, resource *foov1.Foo) (*foov1.Foo, error) {
name, _ := cmd.Flags().GetString("name")
resource.Name = name
// call gRPC update, return updated resource
},
PrintResource: func(_ *cobra.Command, out io.Writer, updated *foov1.Foo) {
fmt.Fprintf(out, "Foo %s updated.\n", updated.GetId())
},
}.CobraCommand(s)For imperative/action commands that don't return a resource (delete, wait, use, set, …).
base.Cmd{
BaseCobraCommand: func() *cobra.Command {
cmd := &cobra.Command{Use: "delete <foo-id>", Short: "Delete a foo", Args: util.ExactArgs(1, "a foo ID")}
cmd.Flags().BoolP("force", "f", false, "Skip confirmation prompt")
return cmd
},
Run: func(s *state.State, cmd *cobra.Command, args []string) error {
force, _ := cmd.Flags().GetBool("force")
if !util.ConfirmAction(force, cmd.ErrOrStderr(), fmt.Sprintf("Delete foo %s?", args[0])) {
fmt.Fprintln(cmd.OutOrStdout(), "Aborted.")
return nil
}
// call gRPC delete
fmt.Fprintf(cmd.OutOrStdout(), "Foo %s deleted.\n", args[0])
return nil
},
}.CobraCommand(s)Key rules:
- JSON output is handled automatically by all base types — never call
output.PrintJSONyourself. - Always read flags via
cmd.Flags().GetString()etc. inRun/Update; do not use cobra bound variables. - Use
util.ExactArgs(n, "description")instead ofcobra.ExactArgsfor better error messages.
All TrimPrefix-based enum formatters live in internal/cmd/output/, grouped by proto package:
| File | Functions |
|---|---|
output/cluster.go |
ClusterPhase, ClusterNodeState, TolerationOperator, TolerationEffect |
output/booking.go |
PackageTier |
output/hybrid.go |
HybridEnvironmentPhase, ClusterCreationStatus, HybridComponentPhase |
output/backup.go |
BackupStatus, BackupScheduleStatus, BackupRestoreStatus |
Each function strips the proto enum prefix via strings.TrimPrefix(x.String(), "PREFIX_"). Functions are named after the type they format, without a redundant String suffix, since the package qualifier already provides context (output.ClusterPhase(...)).
Rules:
- Never inline
strings.TrimPrefix(x.String(), "PREFIX_")in a cmd package. Add a function to the appropriateoutput/*.gofile instead. - Never define a private
phaseString/statusString/ etc. helper in a cmd package for TrimPrefix formatting. These belong inoutput. - Switch-based format/parse pairs (
storageTierString,restartPolicyString, etc.) encode semantic mappings paired with parse functions and belong with their cmd package, not inoutput.
General-purpose output formatting helpers belong in the output package, not in individual cmd packages.
Examples: BoolYesNo (formats a bool as "yes" / "no"), BoolMark, HumanTime, OptionalValue, etc.
Rules:
- If a helper formats a value for display and could be reused across more than one cmd package, add it to
output/. - Never define a private display-formatting helper in a cmd package when it belongs in
output.
Go 1.26 allows passing a literal directly to new, which returns a pointer to it. Use this wherever a pointer to a constant value is needed inline:
req.MultiAz = new(true)
req.Gpu = new(false)
cfg.Version = new("1.13.0")Never add helper functions (boolPtr, stringPtr, intPtr, etc.) for this purpose — they are redundant.