Skip to content

Commit 6ee5e35

Browse files
authored
feat!: merge hybrid cluster commands into cluster, move package to root (#86)
All hybrid cluster management is now unified under `qcloud cluster`. The `qcloud cluster package` subcommand is promoted to `qcloud package`. BREAKING CHANGE: `qcloud hybrid cluster` subcommand has been removed. Use `qcloud cluster create/update/describe/list` instead — hybrid-specific flags (--service-type, --node-selector, --toleration, --topology-spread-constraint, --annotation, --pod-label, --service-annotation, --reserved-cpu-percentage, --reserved-memory-percentage, and storage class flags) are now available directly on the cluster commands. BREAKING CHANGE: `qcloud cluster package` has been removed. Use `qcloud package` instead.
1 parent ea110f3 commit 6ee5e35

40 files changed

+1972
-3532
lines changed

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ debug-run: debug
1414
test:
1515
go test ./...
1616

17+
coverage:
18+
go test -coverpkg=./internal/... -coverprofile=build/coverage.txt -v -race ./...
19+
go tool cover -html=build/coverage.txt
20+
1721
lint:
1822
golangci-lint run
1923

internal/cli/root.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import (
99
"github.com/qdrant/qcloud-cli/internal/cmd/cloudprovider"
1010
"github.com/qdrant/qcloud-cli/internal/cmd/cloudregion"
1111
"github.com/qdrant/qcloud-cli/internal/cmd/cluster"
12-
cmdcontext "github.com/qdrant/qcloud-cli/internal/cmd/context"
12+
contextcmd "github.com/qdrant/qcloud-cli/internal/cmd/context"
1313
"github.com/qdrant/qcloud-cli/internal/cmd/hybrid"
14+
packagecmd "github.com/qdrant/qcloud-cli/internal/cmd/package"
1415
"github.com/qdrant/qcloud-cli/internal/cmd/selfupgrade"
1516
"github.com/qdrant/qcloud-cli/internal/cmd/version"
1617
"github.com/qdrant/qcloud-cli/internal/state"
@@ -67,9 +68,10 @@ Documentation: https://github.com/qdrant/qcloud-cli`,
6768
cmd.AddCommand(cluster.NewCommand(s))
6869
cmd.AddCommand(cloudprovider.NewCommand(s))
6970
cmd.AddCommand(cloudregion.NewCommand(s))
70-
cmd.AddCommand(cmdcontext.NewCommand(s))
71+
cmd.AddCommand(contextcmd.NewCommand(s))
7172
cmd.AddCommand(backup.NewCommand(s))
7273
cmd.AddCommand(hybrid.NewCommand(s))
74+
cmd.AddCommand(packagecmd.NewCommand(s))
7375
cmd.AddCommand(selfupgrade.NewCommand(s))
7476

7577
return cmd

internal/cmd/cluster/cluster.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ func NewCommand(s *state.State) *cobra.Command {
2424
newUnsuspendCommand(s),
2525
newWaitCommand(s),
2626
newVersionCommand(s),
27-
newPackageCommand(s),
2827
newKeyCommand(s),
2928
newScaleCommand(s),
3029
)

internal/cmd/cluster/completion.go

Lines changed: 21 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,8 @@ package cluster
22

33
import (
44
"github.com/spf13/cobra"
5-
6-
"github.com/qdrant/qcloud-cli/internal/cmd/completion"
7-
"github.com/qdrant/qcloud-cli/internal/state"
85
)
96

10-
// cloudProviderFn reads the cloud provider and region from the command's flags.
11-
func cloudProviderFn(cmd *cobra.Command) (string, *string) {
12-
provider, _ := cmd.Flags().GetString("cloud-provider")
13-
region, _ := cmd.Flags().GetString("cloud-region")
14-
if region != "" {
15-
return provider, &region
16-
}
17-
return provider, nil
18-
}
19-
20-
func cpuCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
21-
return completion.CPUCompletion(s, cloudProviderFn)
22-
}
23-
24-
func ramCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
25-
return completion.RAMCompletion(s, cloudProviderFn)
26-
}
27-
28-
func diskCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
29-
return completion.DiskCompletion(s, cloudProviderFn)
30-
}
31-
32-
func gpuCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
33-
return completion.GPUCompletion(s, cloudProviderFn)
34-
}
35-
36-
func packageCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
37-
return completion.PackageNameCompletion(s, cloudProviderFn)
38-
}
39-
407
// diskPerformanceCompletion returns a static completion function for the --disk-performance flag.
418
func diskPerformanceCompletion() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
429
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
@@ -54,6 +21,26 @@ func restartModeCompletion() func(*cobra.Command, []string, string) ([]string, c
5421
// rebalanceStrategyCompletion returns a static completion function for the --rebalance-strategy flag.
5522
func rebalanceStrategyCompletion() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
5623
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
57-
return []string{rebalanceByCount, rebalanceBySize, rebalanceByCountAndSize}, cobra.ShellCompDirectiveNoFileComp
24+
return []string{rebalanceStrategyByCount, rebalanceStrategyBySize, rebalanceStrategyByCountAndSize}, cobra.ShellCompDirectiveNoFileComp
25+
}
26+
}
27+
28+
// dbLogLevelCompletion returns a static completion function for the --db-log-level flag.
29+
func dbLogLevelCompletion() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
30+
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
31+
return []string{dbLogLevelTrace, dbLogLevelDebug, dbLogLevelInfo, dbLogLevelWarn, dbLogLevelError, dbLogLevelOff}, cobra.ShellCompDirectiveNoFileComp
32+
}
33+
}
34+
35+
// auditLogRotationCompletion returns a static completion function for the --audit-log-rotation flag.
36+
func auditLogRotationCompletion() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
37+
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
38+
return []string{auditLogRotationDaily, auditLogRotationHourly}, cobra.ShellCompDirectiveNoFileComp
39+
}
40+
}
41+
42+
func serviceTypeCompletion() func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
43+
return func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
44+
return []string{serviceTypeClusterIP, serviceTypeNodePort, serviceTypeLoadBalancer}, cobra.ShellCompDirectiveNoFileComp
5845
}
5946
}

internal/cmd/cluster/completion_test.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,20 @@ func TestClusterIDCompletion(t *testing.T) {
2424

2525
env.Server.ListClustersCalls.Returns(&clusterv1.ListClustersResponse{
2626
Items: []*clusterv1.Cluster{
27-
{Id: "cluster-abc", Name: "my-cluster"},
28-
{Id: "cluster-xyz", Name: "other-cluster"},
29-
{Id: "cluster-hybrid", Name: "hybrid-cluster", CloudProviderId: "hybrid"},
27+
{Id: "79a272a3-db29-4091-9cd0-5de588da9b2d", Name: "my-cluster", CloudProviderId: "aws"},
28+
{Id: "68c83c58-e9f9-4539-8fa5-20b4e0263117", Name: "other-cluster", CloudProviderId: "gcp"},
29+
{Id: "404adfbf-ad7b-4926-9943-542920be2b78", Name: "hybrid-cluster", CloudProviderId: "hybrid"},
3030
},
3131
}, nil)
3232

3333
stdout, _, err := testutil.Exec(t, env, "__complete", "cluster", "describe", "")
3434
require.NoError(t, err)
35-
assert.Contains(t, stdout, "cluster-abc")
35+
assert.Contains(t, stdout, "79a272a3-db29-4091-9cd0-5de588da9b2d")
3636
assert.Contains(t, stdout, "my-cluster")
37-
assert.Contains(t, stdout, "cluster-xyz")
38-
assert.NotContains(t, stdout, "cluster-hybrid")
37+
assert.Contains(t, stdout, "68c83c58-e9f9-4539-8fa5-20b4e0263117")
38+
assert.Contains(t, stdout, "other-cluster")
39+
assert.Contains(t, stdout, "404adfbf-ad7b-4926-9943-542920be2b78")
40+
assert.Contains(t, stdout, "hybrid-cluster")
3941
}
4042

4143
func TestCloudProviderCompletion(t *testing.T) {
@@ -54,7 +56,7 @@ func TestCloudProviderCompletion(t *testing.T) {
5456
assert.Contains(t, stdout, "aws")
5557
assert.Contains(t, stdout, "Amazon Web Services")
5658
assert.Contains(t, stdout, "gcp")
57-
assert.NotContains(t, stdout, "hybrid")
59+
assert.Contains(t, stdout, "hybrid")
5860
}
5961

6062
func TestCloudRegionCompletion(t *testing.T) {

internal/cmd/cluster/create.go

Lines changed: 43 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99

1010
bookingv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/booking/v1"
1111
clusterv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/cluster/v1"
12-
commonv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/common/v1"
1312

1413
"github.com/qdrant/qcloud-cli/internal/cmd/base"
1514
"github.com/qdrant/qcloud-cli/internal/cmd/clusterutil"
@@ -32,39 +31,42 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 2 -
3231
3332
# Create with labels and extra disk
3433
qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 --ram 32Gi \
35-
--disk 200Gi --label env=production --label team=search`,
34+
--disk 200Gi --label env=production --label team=search
35+
36+
# Create a hybrid cloud cluster with a load balancer service type
37+
qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 2 --ram 8Gi \
38+
--service-type load-balancer
39+
40+
# Create a hybrid cluster with node selectors and tolerations
41+
qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 2 --ram 8Gi \
42+
--node-selector disktype=ssd --toleration "dedicated=qdrant:NoSchedule"
43+
44+
# Create a hybrid cluster with custom storage classes
45+
qcloud cluster create --cloud-provider hybrid --cloud-region my-env --cpu 4 --ram 16Gi \
46+
--database-storage-class fast-ssd --snapshot-storage-class standard`,
3647
BaseCobraCommand: func() *cobra.Command {
3748
cmd := &cobra.Command{
3849
Use: "create",
3950
Short: "Create a new cluster",
4051
Args: cobra.NoArgs,
4152
}
4253
cmd.Flags().String("name", "", "Cluster name (auto-generated if not provided)")
43-
cmd.Flags().String("cloud-provider", "", "Cloud provider ID (required, see 'cluster cloud-provider list)")
44-
cmd.Flags().String("cloud-region", "", "Cloud provider region ID (required, see 'cluster cloud-region list --cloud-provider <provider_id>)")
45-
cmd.Flags().String("version", "", "Qdrant version (default latest)")
54+
cmd.Flags().String("cloud-provider", "", "Cloud provider ID (required, see 'cloud-provider list)")
55+
cmd.Flags().String("cloud-region", "", "Cloud provider region ID (required, see 'cloud-region list --cloud-provider <provider_id>)")
4656
cmd.Flags().Uint32("nodes", 1, "Number of nodes (default 1)")
4757
cmd.Flags().String("package", "", "Booking package name or ID (see 'cluster package list')")
4858
cmd.Flags().Var(new(resource.Millicores), "cpu", "CPU to select a package (e.g. \"1\", \"0.5\", or \"1000m\")")
4959
cmd.Flags().Var(new(resource.ByteQuantity), "ram", "RAM to select a package (e.g. \"8\", \"8G\", \"8Gi\", or \"8GiB\")")
5060
cmd.Flags().Var(new(resource.ByteQuantity), "disk", "Total disk size (e.g. \"200GiB\"); if larger than the package's included disk, the difference is provisioned as additional storage")
5161
cmd.Flags().Var(new(resource.Millicores), "gpu", "Number of GPUs to select a package (e.g. \"1\", \"2\", or \"1000m\")")
5262
cmd.Flags().Bool("multi-az", false, "Require a multi-AZ package")
53-
cmd.Flags().StringArray("label", nil, "Label to apply to the cluster ('key=value'), can be specified multiple times")
5463
cmd.Flags().Bool("wait", false, "Wait for the cluster to become healthy")
5564
cmd.Flags().Duration("wait-timeout", 10*time.Minute, "Maximum time to wait for cluster health")
5665
cmd.Flags().Duration("wait-poll-interval", 5*time.Second, "How often to poll for cluster health")
57-
cmd.Flags().String("disk-performance", "", `Disk performance tier ("balanced", "cost-optimised", "performance")`)
58-
cmd.Flags().Uint32("replication-factor", 0, "Default replication factor for new collections")
59-
cmd.Flags().Int32("write-consistency-factor", 0, "Default write consistency factor for new collections")
60-
cmd.Flags().Bool("async-scorer", false, "Enable async scorer (uses io_uring on Linux)")
61-
cmd.Flags().Int32("optimizer-cpu-budget", 0, `CPU threads for optimization (0=auto, negative=subtract from available CPUs, positive=exact count)`)
62-
cmd.Flags().StringArray("allowed-ip", nil, "Allowed client IP CIDR ranges; max 20")
63-
cmd.Flags().String("restart-mode", "", `Restart policy ("rolling", "parallel", "automatic")`)
64-
cmd.Flags().String("rebalance-strategy", "", `Shard rebalance strategy ("by-count", "by-size", "by-count-and-size")`)
6566
_ = cmd.Flags().MarkHidden("wait-poll-interval")
6667
_ = cmd.MarkFlagRequired("cloud-provider")
6768
_ = cmd.MarkFlagRequired("cloud-region")
69+
addSharedClusterFlags(cmd)
6870
return cmd
6971
},
7072
Run: func(s *state.State, cmd *cobra.Command, args []string) (*clusterv1.Cluster, error) {
@@ -91,11 +93,9 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
9193
}
9294
cloudProvider, _ := cmd.Flags().GetString("cloud-provider")
9395
cloudRegion, _ := cmd.Flags().GetString("cloud-region")
94-
version, _ := cmd.Flags().GetString("version")
9596
nodes, _ := cmd.Flags().GetUint32("nodes")
9697
packageValue, _ := cmd.Flags().GetString("package")
9798
multiAz, _ := cmd.Flags().GetBool("multi-az")
98-
rawLabels, _ := cmd.Flags().GetStringArray("label")
9999

100100
cpuChanged := cmd.Flags().Changed("cpu")
101101
ramChanged := cmd.Flags().Changed("ram")
@@ -124,13 +124,27 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
124124
if util.IsUUID(packageValue) {
125125
packageID = packageValue
126126
if cmd.Flags().Changed("disk") {
127-
pkg, err = clusterutil.ResolvePackageByID(ctx, client.Booking(), accountID, cloudProvider, &cloudRegion, packageValue)
127+
pkg,
128+
err = clusterutil.ResolvePackageByID(ctx,
129+
client.Booking(),
130+
accountID,
131+
cloudProvider,
132+
&cloudRegion,
133+
packageValue,
134+
)
128135
if err != nil {
129136
return nil, err
130137
}
131138
}
132139
} else {
133-
pkg, err = clusterutil.ResolvePackageByName(ctx, client.Booking(), accountID, cloudProvider, &cloudRegion, packageValue)
140+
pkg, err = clusterutil.ResolvePackageByName(
141+
ctx,
142+
client.Booking(),
143+
accountID,
144+
cloudProvider,
145+
&cloudRegion,
146+
packageValue,
147+
)
134148
if err != nil {
135149
return nil, err
136150
}
@@ -159,98 +173,12 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
159173
CloudProviderRegionId: cloudRegion,
160174
Configuration: &clusterv1.ClusterConfiguration{
161175
NumberOfNodes: nodes,
176+
PackageId: packageID,
162177
},
163178
}
164-
if version != "" {
165-
cluster.Configuration.Version = &version
166-
}
167-
if packageID != "" {
168-
cluster.Configuration.PackageId = packageID
169-
}
170-
labelChanges, err := util.ParseLabels(rawLabels)
171-
if err != nil {
172-
return nil, err
173-
}
174-
for k, v := range labelChanges.Set {
175-
cluster.Labels = append(cluster.Labels, &commonv1.KeyValue{Key: k, Value: v})
176-
}
177-
178-
if cmd.Flags().Changed("disk-performance") {
179-
perfStr, _ := cmd.Flags().GetString("disk-performance")
180-
tierType, err := parseDiskPerformance(perfStr)
181-
if err != nil {
182-
return nil, err
183-
}
184-
cluster.Configuration.ClusterStorageConfiguration = &clusterv1.ClusterStorageConfiguration{
185-
StorageTierType: tierType,
186-
}
187-
}
188-
189-
// Database configuration flags
190-
if util.AnyFlagChanged(cmd, []string{"replication-factor", "write-consistency-factor", "async-scorer", "optimizer-cpu-budget"}) {
191-
if cluster.Configuration.DatabaseConfiguration == nil {
192-
cluster.Configuration.DatabaseConfiguration = &clusterv1.DatabaseConfiguration{}
193-
}
194-
dbCfg := cluster.Configuration.DatabaseConfiguration
195-
196-
if util.AnyFlagChanged(cmd, []string{"replication-factor", "write-consistency-factor"}) {
197-
if dbCfg.Collection == nil {
198-
dbCfg.Collection = &clusterv1.DatabaseConfigurationCollection{}
199-
}
200-
if cmd.Flags().Changed("replication-factor") {
201-
v, _ := cmd.Flags().GetUint32("replication-factor")
202-
dbCfg.Collection.ReplicationFactor = &v
203-
}
204-
if cmd.Flags().Changed("write-consistency-factor") {
205-
v, _ := cmd.Flags().GetInt32("write-consistency-factor")
206-
dbCfg.Collection.WriteConsistencyFactor = &v
207-
}
208-
}
209179

210-
if util.AnyFlagChanged(cmd, []string{"async-scorer", "optimizer-cpu-budget"}) {
211-
if dbCfg.Storage == nil {
212-
dbCfg.Storage = &clusterv1.DatabaseConfigurationStorage{}
213-
}
214-
if dbCfg.Storage.Performance == nil {
215-
dbCfg.Storage.Performance = &clusterv1.DatabaseConfigurationStoragePerformance{}
216-
}
217-
if cmd.Flags().Changed("async-scorer") {
218-
v, _ := cmd.Flags().GetBool("async-scorer")
219-
dbCfg.Storage.Performance.AsyncScorer = &v
220-
}
221-
if cmd.Flags().Changed("optimizer-cpu-budget") {
222-
v, _ := cmd.Flags().GetInt32("optimizer-cpu-budget")
223-
dbCfg.Storage.Performance.OptimizerCpuBudget = &v
224-
}
225-
}
226-
}
227-
228-
// Cluster configuration flags
229-
if cmd.Flags().Changed("allowed-ip") {
230-
raw, _ := cmd.Flags().GetStringArray("allowed-ip")
231-
changes, err := util.ParseIPs(raw)
232-
if err != nil {
233-
return nil, err
234-
}
235-
cluster.Configuration.AllowedIpSourceRanges = changes.Add
236-
}
237-
238-
if cmd.Flags().Changed("restart-mode") {
239-
modeStr, _ := cmd.Flags().GetString("restart-mode")
240-
mode, err := parseRestartMode(modeStr)
241-
if err != nil {
242-
return nil, err
243-
}
244-
cluster.Configuration.RestartPolicy = mode.Enum()
245-
}
246-
247-
if cmd.Flags().Changed("rebalance-strategy") {
248-
stratStr, _ := cmd.Flags().GetString("rebalance-strategy")
249-
strat, err := parseRebalanceStrategy(stratStr)
250-
if err != nil {
251-
return nil, err
252-
}
253-
cluster.Configuration.RebalanceStrategy = strat.Enum()
180+
if err := applySharedClusterFlags(cmd, cluster); err != nil {
181+
return nil, err
254182
}
255183

256184
if cmd.Flags().Changed("disk") && pkg != nil {
@@ -295,14 +223,17 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
295223
}.CobraCommand(s)
296224
_ = cmd.RegisterFlagCompletionFunc("cloud-provider", completion.CloudProviderCompletion(s))
297225
_ = cmd.RegisterFlagCompletionFunc("cloud-region", completion.CloudRegionCompletion(s))
298-
_ = cmd.RegisterFlagCompletionFunc("package", packageCompletion(s))
226+
_ = cmd.RegisterFlagCompletionFunc("package", completion.PackageNameCompletion(s))
299227
_ = cmd.RegisterFlagCompletionFunc("version", completion.VersionCompletion(s))
300-
_ = cmd.RegisterFlagCompletionFunc("cpu", cpuCompletion(s))
301-
_ = cmd.RegisterFlagCompletionFunc("ram", ramCompletion(s))
302-
_ = cmd.RegisterFlagCompletionFunc("disk", diskCompletion(s))
303-
_ = cmd.RegisterFlagCompletionFunc("gpu", gpuCompletion(s))
228+
_ = cmd.RegisterFlagCompletionFunc("cpu", completion.CPUCompletion(s))
229+
_ = cmd.RegisterFlagCompletionFunc("ram", completion.RAMCompletion(s))
230+
_ = cmd.RegisterFlagCompletionFunc("disk", completion.DiskCompletion(s))
231+
_ = cmd.RegisterFlagCompletionFunc("gpu", completion.GPUCompletion(s))
304232
_ = cmd.RegisterFlagCompletionFunc("disk-performance", diskPerformanceCompletion())
305233
_ = cmd.RegisterFlagCompletionFunc("restart-mode", restartModeCompletion())
306234
_ = cmd.RegisterFlagCompletionFunc("rebalance-strategy", rebalanceStrategyCompletion())
235+
_ = cmd.RegisterFlagCompletionFunc("service-type", serviceTypeCompletion())
236+
_ = cmd.RegisterFlagCompletionFunc("db-log-level", dbLogLevelCompletion())
237+
_ = cmd.RegisterFlagCompletionFunc("audit-log-rotation", auditLogRotationCompletion())
307238
return cmd
308239
}

0 commit comments

Comments
 (0)