Skip to content

Commit ea110f3

Browse files
Davidoniumqdrant-cloud-botQdrant Claw
authored
feat: add missing flags to the hybrid environment clusters (#76)
* feat: add missing flags to the hybrid environment clusters * refactor: add resources flags to hybrid cluster command and calculate package / additional disk changes * refactor: remove gpu related flags on the hybrid cloud cluster commands, refactor package resolving so that it uses a struct instead of a thousand parameters * refactor: move wait flags to the end, remove gpu-type flag for now * refactor: make use of util.AnyFlagChanged in cluster create * test: add tests for hybrid cloud environment extra flags (#85) Cover the new flags added to hybrid cluster create, update, and describe commands: package resolution (by name, CPU/RAM, mutual exclusivity, disk calculation), cluster config (restart-policy, rebalance-strategy, topology-spread-constraint, cost-allocation-label), storage config (all 4 storage class flags), and database config (db-log-level, vectors-on-disk, TLS, secrets, audit logging). Also adds unit tests for the new helper parsers/formatters, CalculateAdditionalDisk, util.IsUUID, and util.AnyFlagChanged. Fixes existing create tests that broke due to mandatory package resolution. Made-with: Cursor Co-authored-by: Qdrant Claw <qdrant-claw@qdrant.com> --------- Co-authored-by: qdrant-cloud-bot <111755117+qdrant-cloud-bot@users.noreply.github.com> Co-authored-by: Qdrant Claw <qdrant-claw@qdrant.com>
1 parent 70320ce commit ea110f3

28 files changed

+2357
-483
lines changed

internal/cmd/cluster/completion.go

Lines changed: 12 additions & 257 deletions
Original file line numberDiff line numberDiff line change
@@ -1,238 +1,40 @@
11
package cluster
22

33
import (
4-
"fmt"
5-
64
"github.com/spf13/cobra"
75

8-
bookingv1 "github.com/qdrant/qdrant-cloud-public-api/gen/go/qdrant/cloud/booking/v1"
9-
10-
"github.com/qdrant/qcloud-cli/internal/cmd/output"
11-
"github.com/qdrant/qcloud-cli/internal/resource"
6+
"github.com/qdrant/qcloud-cli/internal/cmd/completion"
127
"github.com/qdrant/qcloud-cli/internal/state"
138
)
149

15-
// packageFilter holds the parameters for filtering packages.
16-
// Zero values for CPU/RAM/GPU mean "do not filter on that dimension".
17-
type packageFilter struct {
18-
CPU resource.Millicores
19-
RAM resource.ByteQuantity
20-
GPU resource.Millicores
21-
IncludeGPU bool
22-
MultiAz bool
23-
}
24-
25-
// filteredPackages fetches active packages matching the given filter.
26-
// Returns nil (no completions) if --cloud-provider is not set.
27-
func filteredPackages(cmd *cobra.Command, s *state.State, f packageFilter) ([]*bookingv1.Package, error) {
10+
// cloudProviderFn reads the cloud provider and region from the command's flags.
11+
func cloudProviderFn(cmd *cobra.Command) (string, *string) {
2812
provider, _ := cmd.Flags().GetString("cloud-provider")
29-
if provider == "" {
30-
return nil, nil
31-
}
32-
33-
ctx := cmd.Context()
34-
client, err := s.Client(ctx)
35-
if err != nil {
36-
return nil, err
37-
}
38-
39-
accountID, err := s.AccountID()
40-
if err != nil {
41-
return nil, err
42-
}
43-
4413
region, _ := cmd.Flags().GetString("cloud-region")
45-
req := &bookingv1.ListPackagesRequest{
46-
AccountId: accountID,
47-
CloudProviderId: provider,
48-
Statuses: []bookingv1.PackageStatus{bookingv1.PackageStatus_PACKAGE_STATUS_ACTIVE},
49-
Gpu: &f.IncludeGPU,
50-
}
5114
if region != "" {
52-
req.CloudProviderRegionId = &region
53-
}
54-
if f.MultiAz {
55-
req.MultiAz = new(true)
56-
}
57-
58-
resp, err := client.Booking().ListPackages(ctx, req)
59-
if err != nil {
60-
return nil, err
61-
}
62-
63-
var result []*bookingv1.Package
64-
for _, p := range resp.GetItems() {
65-
rc := p.GetResourceConfiguration()
66-
if f.CPU != 0 {
67-
pkgCPU, _ := resource.ParseMillicores(rc.GetCpu())
68-
if pkgCPU != f.CPU {
69-
continue
70-
}
71-
}
72-
if f.RAM != 0 {
73-
pkgRAM, _ := resource.ParseByteQuantity(rc.GetRam())
74-
if pkgRAM != f.RAM {
75-
continue
76-
}
77-
}
78-
if f.GPU != 0 {
79-
pkgGPU, _ := resource.ParseMillicores(rc.GetGpu())
80-
if pkgGPU != f.GPU {
81-
continue
82-
}
83-
}
84-
result = append(result, p)
15+
return provider, &region
8516
}
86-
return result, nil
17+
return provider, nil
8718
}
8819

89-
// cpuCompletion returns a completion function for the --cpu flag.
9020
func cpuCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
91-
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
92-
provider, _ := cmd.Flags().GetString("cloud-provider")
93-
if provider == "" {
94-
return nil, cobra.ShellCompDirectiveNoFileComp
95-
}
96-
ram := *cmd.Flags().Lookup("ram").Value.(*resource.ByteQuantity)
97-
gpu := *cmd.Flags().Lookup("gpu").Value.(*resource.Millicores)
98-
multiAz, _ := cmd.Flags().GetBool("multi-az")
99-
pkgs, err := filteredPackages(cmd, s, packageFilter{
100-
RAM: ram,
101-
GPU: gpu,
102-
IncludeGPU: gpu != 0,
103-
MultiAz: multiAz,
104-
})
105-
if err != nil {
106-
return nil, cobra.ShellCompDirectiveError
107-
}
108-
109-
seen := make(map[resource.Millicores]struct{})
110-
var completions []string
111-
for _, p := range pkgs {
112-
v, err := resource.ParseMillicores(p.GetResourceConfiguration().GetCpu())
113-
if err != nil {
114-
cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err))
115-
continue
116-
}
117-
if _, ok := seen[v]; !ok {
118-
seen[v] = struct{}{}
119-
completions = append(completions, v.String())
120-
}
121-
}
122-
return completions, cobra.ShellCompDirectiveNoFileComp
123-
}
21+
return completion.CPUCompletion(s, cloudProviderFn)
12422
}
12523

126-
// ramCompletion returns a completion function for the --ram flag.
12724
func ramCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
128-
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
129-
provider, _ := cmd.Flags().GetString("cloud-provider")
130-
if provider == "" {
131-
return nil, cobra.ShellCompDirectiveNoFileComp
132-
}
133-
cpu := *cmd.Flags().Lookup("cpu").Value.(*resource.Millicores)
134-
gpu := *cmd.Flags().Lookup("gpu").Value.(*resource.Millicores)
135-
multiAz, _ := cmd.Flags().GetBool("multi-az")
136-
pkgs, err := filteredPackages(cmd, s, packageFilter{
137-
CPU: cpu,
138-
GPU: gpu,
139-
IncludeGPU: gpu != 0,
140-
MultiAz: multiAz,
141-
})
142-
if err != nil {
143-
return nil, cobra.ShellCompDirectiveError
144-
}
145-
146-
seen := make(map[resource.ByteQuantity]struct{})
147-
var completions []string
148-
for _, p := range pkgs {
149-
v, err := resource.ParseByteQuantity(p.GetResourceConfiguration().GetRam())
150-
if err != nil {
151-
cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err))
152-
continue
153-
}
154-
if _, ok := seen[v]; !ok {
155-
seen[v] = struct{}{}
156-
completions = append(completions, resource.FormatByteQuantity(v, resource.UnitGiB))
157-
}
158-
}
159-
return completions, cobra.ShellCompDirectiveNoFileComp
160-
}
25+
return completion.RAMCompletion(s, cloudProviderFn)
16126
}
16227

163-
// diskCompletion returns a completion function for the --disk flag.
16428
func diskCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
165-
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
166-
provider, _ := cmd.Flags().GetString("cloud-provider")
167-
if provider == "" {
168-
return nil, cobra.ShellCompDirectiveNoFileComp
169-
}
170-
cpu := *cmd.Flags().Lookup("cpu").Value.(*resource.Millicores)
171-
ram := *cmd.Flags().Lookup("ram").Value.(*resource.ByteQuantity)
172-
gpu := *cmd.Flags().Lookup("gpu").Value.(*resource.Millicores)
173-
multiAz, _ := cmd.Flags().GetBool("multi-az")
174-
pkgs, err := filteredPackages(cmd, s, packageFilter{
175-
CPU: cpu,
176-
RAM: ram,
177-
GPU: gpu,
178-
IncludeGPU: gpu != 0,
179-
MultiAz: multiAz,
180-
})
181-
if err != nil {
182-
return nil, cobra.ShellCompDirectiveError
183-
}
184-
185-
seen := make(map[resource.ByteQuantity]struct{})
186-
var completions []string
187-
for _, p := range pkgs {
188-
v, err := resource.ParseByteQuantity(p.GetResourceConfiguration().GetDisk())
189-
if err != nil {
190-
cobra.CompErrorln(fmt.Sprintf("package %s: %v", p.GetName(), err))
191-
continue
192-
}
193-
if _, ok := seen[v]; !ok {
194-
seen[v] = struct{}{}
195-
completions = append(completions, v.String())
196-
}
197-
}
198-
return completions, cobra.ShellCompDirectiveNoFileComp
199-
}
29+
return completion.DiskCompletion(s, cloudProviderFn)
20030
}
20131

202-
// gpuCompletion returns a completion function for the --gpu flag.
20332
func gpuCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
204-
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
205-
provider, _ := cmd.Flags().GetString("cloud-provider")
206-
if provider == "" {
207-
return nil, cobra.ShellCompDirectiveNoFileComp
208-
}
209-
cpu := *cmd.Flags().Lookup("cpu").Value.(*resource.Millicores)
210-
ram := *cmd.Flags().Lookup("ram").Value.(*resource.ByteQuantity)
211-
multiAz, _ := cmd.Flags().GetBool("multi-az")
212-
pkgs, err := filteredPackages(cmd, s, packageFilter{
213-
CPU: cpu,
214-
RAM: ram,
215-
IncludeGPU: true,
216-
MultiAz: multiAz,
217-
})
218-
if err != nil {
219-
return nil, cobra.ShellCompDirectiveError
220-
}
33+
return completion.GPUCompletion(s, cloudProviderFn)
34+
}
22135

222-
seen := make(map[resource.Millicores]struct{})
223-
var completions []string
224-
for _, p := range pkgs {
225-
m, err := resource.ParseMillicores(p.GetResourceConfiguration().GetGpu())
226-
if err != nil || m <= 0 || int64(m)%1000 != 0 {
227-
continue
228-
}
229-
if _, ok := seen[m]; !ok {
230-
seen[m] = struct{}{}
231-
completions = append(completions, m.String())
232-
}
233-
}
234-
return completions, cobra.ShellCompDirectiveNoFileComp
235-
}
36+
func packageCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
37+
return completion.PackageNameCompletion(s, cloudProviderFn)
23638
}
23739

23840
// diskPerformanceCompletion returns a static completion function for the --disk-performance flag.
@@ -255,50 +57,3 @@ func rebalanceStrategyCompletion() func(*cobra.Command, []string, string) ([]str
25557
return []string{rebalanceByCount, rebalanceBySize, rebalanceByCountAndSize}, cobra.ShellCompDirectiveNoFileComp
25658
}
25759
}
258-
259-
// packageCompletion returns a completion function for the --package flag.
260-
func packageCompletion(s *state.State) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) {
261-
return func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
262-
provider, _ := cmd.Flags().GetString("cloud-provider")
263-
if provider == "" {
264-
return nil, cobra.ShellCompDirectiveNoFileComp
265-
}
266-
267-
ctx := cmd.Context()
268-
client, err := s.Client(ctx)
269-
if err != nil {
270-
return nil, cobra.ShellCompDirectiveError
271-
}
272-
273-
accountID, err := s.AccountID()
274-
if err != nil {
275-
return nil, cobra.ShellCompDirectiveError
276-
}
277-
278-
region, _ := cmd.Flags().GetString("cloud-region")
279-
req := &bookingv1.ListPackagesRequest{
280-
AccountId: accountID,
281-
CloudProviderId: provider,
282-
Statuses: []bookingv1.PackageStatus{bookingv1.PackageStatus_PACKAGE_STATUS_ACTIVE},
283-
}
284-
if region != "" {
285-
req.CloudProviderRegionId = &region
286-
}
287-
288-
resp, err := client.Booking().ListPackages(ctx, req)
289-
if err != nil {
290-
return nil, cobra.ShellCompDirectiveError
291-
}
292-
293-
completions := make([]string, 0, len(resp.GetItems()))
294-
for _, p := range resp.GetItems() {
295-
desc := output.PackageTier(p.GetTier())
296-
if rc := p.GetResourceConfiguration(); rc != nil {
297-
desc += fmt.Sprintf(" | %s RAM / %s CPU / %s disk", rc.GetRam(), rc.GetCpu(), rc.GetDisk())
298-
}
299-
desc += " | " + formatMillicents(p.GetUnitIntPricePerHour(), p.GetCurrency()) + "/hr"
300-
completions = append(completions, p.GetName()+"\t"+desc)
301-
}
302-
return completions, cobra.ShellCompDirectiveNoFileComp
303-
}
304-
}

internal/cmd/cluster/create.go

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,23 +121,31 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
121121
var packageID string
122122

123123
if packageValue != "" {
124-
if isUUID(packageValue) {
124+
if util.IsUUID(packageValue) {
125125
packageID = packageValue
126126
if cmd.Flags().Changed("disk") {
127-
pkg, err = resolvePackageByID(ctx, client.Booking(), accountID, cloudProvider, cloudRegion, packageValue)
127+
pkg, err = clusterutil.ResolvePackageByID(ctx, client.Booking(), accountID, cloudProvider, &cloudRegion, packageValue)
128128
if err != nil {
129129
return nil, err
130130
}
131131
}
132132
} else {
133-
pkg, err = resolvePackageByName(ctx, client.Booking(), accountID, cloudProvider, cloudRegion, packageValue)
133+
pkg, err = clusterutil.ResolvePackageByName(ctx, client.Booking(), accountID, cloudProvider, &cloudRegion, packageValue)
134134
if err != nil {
135135
return nil, err
136136
}
137137
packageID = pkg.GetId()
138138
}
139139
} else {
140-
pkg, err = resolvePackageByResources(ctx, client.Booking(), accountID, cloudProvider, cloudRegion, cpu, gpu, ram, multiAz)
140+
pkg, err = clusterutil.ResolvePackageByResources(ctx, client.Booking(), clusterutil.PackageResourceQuery{
141+
AccountID: accountID,
142+
CloudProvider: cloudProvider,
143+
CloudRegion: &cloudRegion,
144+
CPU: cpu,
145+
GPU: gpu,
146+
RAM: ram,
147+
MultiAz: multiAz,
148+
})
141149
if err != nil {
142150
return nil, err
143151
}
@@ -179,14 +187,13 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
179187
}
180188

181189
// Database configuration flags
182-
if cmd.Flags().Changed("replication-factor") || cmd.Flags().Changed("write-consistency-factor") ||
183-
cmd.Flags().Changed("async-scorer") || cmd.Flags().Changed("optimizer-cpu-budget") {
190+
if util.AnyFlagChanged(cmd, []string{"replication-factor", "write-consistency-factor", "async-scorer", "optimizer-cpu-budget"}) {
184191
if cluster.Configuration.DatabaseConfiguration == nil {
185192
cluster.Configuration.DatabaseConfiguration = &clusterv1.DatabaseConfiguration{}
186193
}
187194
dbCfg := cluster.Configuration.DatabaseConfiguration
188195

189-
if cmd.Flags().Changed("replication-factor") || cmd.Flags().Changed("write-consistency-factor") {
196+
if util.AnyFlagChanged(cmd, []string{"replication-factor", "write-consistency-factor"}) {
190197
if dbCfg.Collection == nil {
191198
dbCfg.Collection = &clusterv1.DatabaseConfigurationCollection{}
192199
}
@@ -200,7 +207,7 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
200207
}
201208
}
202209

203-
if cmd.Flags().Changed("async-scorer") || cmd.Flags().Changed("optimizer-cpu-budget") {
210+
if util.AnyFlagChanged(cmd, []string{"async-scorer", "optimizer-cpu-budget"}) {
204211
if dbCfg.Storage == nil {
205212
dbCfg.Storage = &clusterv1.DatabaseConfigurationStorage{}
206213
}
@@ -248,17 +255,13 @@ qcloud cluster create --cloud-provider aws --cloud-region eu-central-1 --cpu 4 -
248255

249256
if cmd.Flags().Changed("disk") && pkg != nil {
250257
requestedDisk := *cmd.Flags().Lookup("disk").Value.(*resource.ByteQuantity)
251-
if pkgDiskStr := pkg.GetResourceConfiguration().GetDisk(); pkgDiskStr != "" {
252-
pkgDisk, err := resource.ParseByteQuantity(pkgDiskStr)
253-
if err != nil {
254-
return nil, err
255-
}
256-
257-
// only apply additional disk calculation if requested disk is bigger than the disk package
258-
if requestedDisk > pkgDisk {
259-
cluster.Configuration.AdditionalResources = &clusterv1.AdditionalResources{
260-
Disk: uint32(requestedDisk.GiB() - pkgDisk.GiB()), // API expects additional disk in GiB
261-
}
258+
additionalDisk, err := clusterutil.CalculateAdditionalDisk(requestedDisk, pkg)
259+
if err != nil {
260+
return nil, err
261+
}
262+
if additionalDisk > 0 {
263+
cluster.Configuration.AdditionalResources = &clusterv1.AdditionalResources{
264+
Disk: additionalDisk,
262265
}
263266
}
264267
}

0 commit comments

Comments
 (0)