From b01cf5345927ef04ba7a64c50df3c417218b4c0f Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:09:11 +0100 Subject: [PATCH 01/14] feat(oidc): mark subject-token, organisation-id, and service-account-id as required flags --- cmd/oidc/token-exchange.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/oidc/token-exchange.go b/cmd/oidc/token-exchange.go index 8868571..ce9acea 100644 --- a/cmd/oidc/token-exchange.go +++ b/cmd/oidc/token-exchange.go @@ -147,4 +147,9 @@ func init() { tokenExchangeCmd.Flags().StringVar(&organisationIDFlag, "organisation-id", "", "Organisation ID (can also be set via context)") tokenExchangeCmd.Flags().StringVar(&serviceAccountIDFlag, "service-account-id", "", "Service account ID (can also be set via THALASSA_SERVICE_ACCOUNT_ID env var)") tokenExchangeCmd.Flags().StringVar(&accessTokenLifetimeFlag, "access-token-lifetime", "1h", "Access token lifetime (min: 1m, max: 24h, default: 1h)") + + // mark required flags + _ = tokenExchangeCmd.MarkFlagRequired("subject-token") + _ = tokenExchangeCmd.MarkFlagRequired("organisation-id") + _ = tokenExchangeCmd.MarkFlagRequired("service-account-id") } From 852026ad03d7a91e1b63cbc4292d290b4fe72158 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:09:47 +0100 Subject: [PATCH 02/14] refactor: add internal helper functions to find vpc or subnet by id, name or slug --- internal/iaas/regions.go | 217 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 internal/iaas/regions.go diff --git a/internal/iaas/regions.go b/internal/iaas/regions.go new file mode 100644 index 0000000..c6d668c --- /dev/null +++ b/internal/iaas/regions.go @@ -0,0 +1,217 @@ +package iaas + +import ( + "context" + "fmt" + "strings" + + "github.com/thalassa-cloud/client-go/iaas" + "github.com/thalassa-cloud/client-go/pkg/client" +) + +// FindRegionByIdentitySlugOrName finds a region in the given list by matching against +// identity, slug, or name (case-insensitive). Prefers identity/slug (unique) over name. +// Returns the matching region or nil if not found. +func FindRegionByIdentitySlugOrName(regions []iaas.Region, search string) *iaas.Region { + if search == "" { + return nil + } + + // First, try to match by identity or slug (unique identifiers) + for _, r := range regions { + if strings.EqualFold(r.Identity, search) || strings.EqualFold(r.Slug, search) { + return &r + } + } + + // If no match by identity/slug, try by name + for _, r := range regions { + if strings.EqualFold(r.Name, search) { + return &r + } + } + return nil +} + +// FindRegionByIdentitySlugOrNameWithError finds a region in the given list by matching against +// identity, slug, or name (case-insensitive). Returns the matching region or an error with +// available region slugs if not found. +func FindRegionByIdentitySlugOrNameWithError(regions []iaas.Region, search string) (*iaas.Region, error) { + region := FindRegionByIdentitySlugOrName(regions, search) + if region == nil { + availableRegions := make([]string, 0, len(regions)) + for _, r := range regions { + availableRegions = append(availableRegions, r.Slug) + } + return nil, fmt.Errorf("region not found: %s, available regions: %s", search, strings.Join(availableRegions, ", ")) + } + return region, nil +} + +// FindVPCByIdentitySlugOrName finds a VPC in the given list by matching against +// identity, slug, or name (case-insensitive). Prefers identity/slug (unique) over name. +// Returns the matching VPC or nil if not found. +func FindVPCByIdentitySlugOrName(vpcs []iaas.Vpc, search string) *iaas.Vpc { + if search == "" { + return nil + } + + // First, try to match by identity or slug (unique identifiers) + for _, v := range vpcs { + if strings.EqualFold(v.Identity, search) || strings.EqualFold(v.Slug, search) { + return &v + } + } + + // If no match by identity/slug, try by name + for _, v := range vpcs { + if strings.EqualFold(v.Name, search) { + return &v + } + } + return nil +} + +// FindVPCByIdentitySlugOrNameWithError finds a VPC in the given list by matching against +// identity, slug, or name (case-insensitive). Returns the matching VPC or an error if not found. +func FindVPCByIdentitySlugOrNameWithError(vpcs []iaas.Vpc, search string) (*iaas.Vpc, error) { + vpc := FindVPCByIdentitySlugOrName(vpcs, search) + if vpc == nil { + return nil, fmt.Errorf("vpc not found: %s", search) + } + return vpc, nil +} + +// GetVPCByIdentitySlugOrName attempts to get a VPC by first trying GetVpc (for identity), +// and if that fails with NotFound, falls back to listing all VPCs and searching by identity, slug, or name. +// This is more efficient than always listing all VPCs when the identifier is likely an identity. +func GetVPCByIdentitySlugOrName(ctx context.Context, iaasClient *iaas.Client, search string) (*iaas.Vpc, error) { + if search == "" { + return nil, fmt.Errorf("vpc identifier is required") + } + + // First, try to get the VPC directly (most efficient for identities) + vpc, err := iaasClient.GetVpc(ctx, search) + if err == nil { + return vpc, nil + } + + // If not found, try searching by slug or name + if !client.IsNotFound(err) { + return nil, fmt.Errorf("failed to get vpc: %w", err) + } + + // Fall back to listing and searching + vpcs, err := iaasClient.ListVpcs(ctx, &iaas.ListVpcsRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to list vpcs: %w", err) + } + + vpc = FindVPCByIdentitySlugOrName(vpcs, search) + if vpc == nil { + return nil, fmt.Errorf("vpc not found: %s", search) + } + return vpc, nil +} + +// FindSubnetByIdentitySlugOrName finds a subnet in the given list by matching against +// identity, slug, or name (case-insensitive). Prefers identity/slug (unique) over name. +// Returns the matching subnet or nil if not found. +func FindSubnetByIdentitySlugOrName(subnets []iaas.Subnet, search string) *iaas.Subnet { + if search == "" { + return nil + } + + // First, try to match by identity or slug (unique identifiers) + for _, s := range subnets { + if strings.EqualFold(s.Identity, search) || strings.EqualFold(s.Slug, search) { + return &s + } + } + + // If no match by identity/slug, try by name + for _, s := range subnets { + if strings.EqualFold(s.Name, search) { + return &s + } + } + return nil +} + +// FindSubnetByIdentitySlugOrNameWithError finds a subnet in the given list by matching against +// identity, slug, or name (case-insensitive). Prefers identity/slug (unique) over name. +// Returns the matching subnet or an error if not found. +func FindSubnetByIdentitySlugOrNameWithError(subnets []iaas.Subnet, search string) (*iaas.Subnet, error) { + subnet := FindSubnetByIdentitySlugOrName(subnets, search) + if subnet == nil { + return nil, fmt.Errorf("subnet not found: %s", search) + } + return subnet, nil +} + +// GetSubnetByIdentitySlugOrName attempts to get a subnet by first trying GetSubnet (for identity), +// and if that fails with NotFound, falls back to listing all subnets and searching by identity, slug, or name. +// This is more efficient than always listing all subnets when the identifier is likely an identity. +func GetSubnetByIdentitySlugOrName(ctx context.Context, iaasClient *iaas.Client, search string) (*iaas.Subnet, error) { + if search == "" { + return nil, fmt.Errorf("subnet identifier is required") + } + + // First, try to get the subnet directly (most efficient for identities) + subnet, err := iaasClient.GetSubnet(ctx, search) + if err == nil { + return subnet, nil + } + + // If not found, try searching by slug or name + if !client.IsNotFound(err) { + return nil, fmt.Errorf("failed to get subnet: %w", err) + } + + // Fall back to listing and searching + subnets, err := iaasClient.ListSubnets(ctx, &iaas.ListSubnetsRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to list subnets: %w", err) + } + + subnet = FindSubnetByIdentitySlugOrName(subnets, search) + if subnet == nil { + return nil, fmt.Errorf("subnet not found: %s", search) + } + return subnet, nil +} + +// FindVolumeTypeByIdentitySlugOrName finds a volume type in the given list by matching against +// identity or name (case-insensitive). Prefers identity (unique) over name. +// Returns the matching volume type or nil if not found. +func FindVolumeTypeByIdentitySlugOrName(volumeTypes []iaas.VolumeType, search string) *iaas.VolumeType { + if search == "" { + return nil + } + + // First, try to match by identity (unique identifier) + for _, vt := range volumeTypes { + if strings.EqualFold(vt.Identity, search) { + return &vt + } + } + + // If no match by identity, try by name + for _, vt := range volumeTypes { + if strings.EqualFold(vt.Name, search) { + return &vt + } + } + return nil +} + +// FindVolumeTypeByIdentitySlugOrNameWithError finds a volume type in the given list by matching against +// identity or name (case-insensitive). Prefers identity (unique) over name. +// Returns the matching volume type or an error if not found. +func FindVolumeTypeByIdentitySlugOrNameWithError(volumeTypes []iaas.VolumeType, search string) (*iaas.VolumeType, error) { + volumeType := FindVolumeTypeByIdentitySlugOrName(volumeTypes, search) + if volumeType == nil { + return nil, fmt.Errorf("volume type not found: %s", search) + } + return volumeType, nil +} From bce2f76ae3efe284fd1f769671ca2ced6ba1db5c Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:10:03 +0100 Subject: [PATCH 03/14] chore(completions): various new completion helpers --- internal/completion/completion.go | 107 ++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/internal/completion/completion.go b/internal/completion/completion.go index 92bfab1..49d0d78 100644 --- a/internal/completion/completion.go +++ b/internal/completion/completion.go @@ -2,6 +2,7 @@ package completion import ( "fmt" + "strings" "github.com/spf13/cobra" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -331,6 +332,112 @@ func CompleteKubernetesVersion(cmd *cobra.Command, args []string, toComplete str return completions, cobra.ShellCompDirectiveNoFileComp } +// CompleteDbEngineVersion provides completion for DBaaS engine versions +// It requires the --engine flag to be set to determine which engine versions to return +func CompleteDbEngineVersion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Get the engine flag value + engineFlag, err := cmd.Flags().GetString("engine") + if err != nil || engineFlag == "" { + // If engine is not set, return empty (user needs to set engine first) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + client, err := thalassaclient.GetThalassaClient() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + // Parse the engine type + engine := dbaas.DbClusterDatabaseEngine(engineFlag) + + versions, err := client.DBaaS().ListEngineVersions(cmd.Context(), engine, &dbaas.ListEngineVersionsRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + completions := make([]string, 0) + for _, v := range versions { + desc := fmt.Sprintf("%s (%d.%d)", v.EngineVersion, v.MajorVersion, v.MinorVersion) + completions = append(completions, v.Identity+"\t"+desc) + completions = append(completions, v.EngineVersion+"\t"+desc) + } + return completions, cobra.ShellCompDirectiveNoFileComp +} + +// CompleteDbInstanceType provides completion for DBaaS instance types +func CompleteDbInstanceType(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client, err := thalassaclient.GetThalassaClient() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + instanceTypes, err := client.DBaaS().ListDatabaseInstanceTypes(cmd.Context(), &dbaas.ListDatabaseInstanceTypesRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + completions := make([]string, 0) + for _, it := range instanceTypes { + desc := fmt.Sprintf("%d vCPU, %d GB RAM (%s)", it.Cpus, it.Memory, it.CategorySlug) + completions = append(completions, it.Identity+"\t"+desc) + completions = append(completions, it.Name+"\t"+desc) + } + return completions, cobra.ShellCompDirectiveNoFileComp +} + +// CompleteKubernetesNodePool provides completion for Kubernetes node pools +// It requires the --cluster flag to be set to determine which node pools to return +func CompleteKubernetesNodePool(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Get the cluster flag value + clusterFlag, err := cmd.Flags().GetString("cluster") + if err != nil || clusterFlag == "" { + // If cluster is not set, return empty (user needs to set cluster first) + return nil, cobra.ShellCompDirectiveNoFileComp + } + + client, err := thalassaclient.GetThalassaClient() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + // Resolve cluster by identity, name, or slug + clusters, err := client.Kubernetes().ListKubernetesClusters(cmd.Context(), &kubernetes.ListKubernetesClustersRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var cluster *kubernetes.KubernetesCluster + for _, c := range clusters { + if strings.EqualFold(c.Identity, clusterFlag) || strings.EqualFold(c.Name, clusterFlag) || strings.EqualFold(c.Slug, clusterFlag) { + cluster = &c + break + } + } + + if cluster == nil { + return nil, cobra.ShellCompDirectiveNoFileComp + } + + // List node pools for the cluster + nodePools, err := client.Kubernetes().ListKubernetesNodePools(cmd.Context(), cluster.Identity, &kubernetes.ListKubernetesNodePoolsRequest{}) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + completions := make([]string, 0) + for _, np := range nodePools { + desc := fmt.Sprintf("%s (%s)", np.Name, np.Status) + completions = append(completions, np.Identity+"\t"+desc) + if np.Name != "" && np.Name != np.Identity { + completions = append(completions, np.Name+"\t"+desc) + } + if np.Slug != "" && np.Slug != np.Identity && np.Slug != np.Name { + completions = append(completions, np.Slug+"\t"+desc) + } + } + return completions, cobra.ShellCompDirectiveNoFileComp +} + // CompleteKubernetesCluster provides completion for Kubernetes cluster identities, names, and slugs func CompleteKubernetesCluster(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { client, err := thalassaclient.GetThalassaClient() From 4b0ef625772629274edc39ad3f3b87eee3f3f27a Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:10:23 +0100 Subject: [PATCH 04/14] refactor(kubernetes): use flag var --- cmd/kubernetes/nodepools/update.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/kubernetes/nodepools/update.go b/cmd/kubernetes/nodepools/update.go index e804bd4..0107467 100644 --- a/cmd/kubernetes/nodepools/update.go +++ b/cmd/kubernetes/nodepools/update.go @@ -292,7 +292,7 @@ func init() { updateCmd.Flags().StringSliceVar(&updateNodePoolSecurityGroups, "security-groups", []string{}, "Security group identities to attach to node pool machines") updateCmd.Flags().BoolVar(&updateNodePoolWait, "wait", false, "Wait for the node pool update to complete") - updateCmd.RegisterFlagCompletionFunc("cluster", completion.CompleteKubernetesCluster) + updateCmd.RegisterFlagCompletionFunc(ClusterFlag, completion.CompleteKubernetesCluster) updateCmd.RegisterFlagCompletionFunc("machine-type", completion.CompleteMachineType) updateCmd.RegisterFlagCompletionFunc("upgrade-strategy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"manual", "auto", "always", "on-delete", "inplace", "never"}, cobra.ShellCompDirectiveNoFileComp From be2d44edafc0d287fead3920d1691b72caa76bf9 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:10:46 +0100 Subject: [PATCH 05/14] feat(nodepools): add flag completions for cluster, subnet and VPC ID --- cmd/kubernetes/nodepools/create.go | 1 + cmd/kubernetes/nodepools/delete.go | 19 ++++++++++--------- cmd/kubernetes/nodepools/list.go | 5 +++++ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/cmd/kubernetes/nodepools/create.go b/cmd/kubernetes/nodepools/create.go index 705df76..a0279c4 100644 --- a/cmd/kubernetes/nodepools/create.go +++ b/cmd/kubernetes/nodepools/create.go @@ -225,6 +225,7 @@ func init() { createCmd.RegisterFlagCompletionFunc("cluster", completion.CompleteKubernetesCluster) createCmd.RegisterFlagCompletionFunc("machine-type", completion.CompleteMachineType) + createCmd.RegisterFlagCompletionFunc("subnet", completion.CompleteSubnetEnhanced) createCmd.RegisterFlagCompletionFunc("upgrade-strategy", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return []string{"manual", "auto", "always", "on-delete", "inplace", "never"}, cobra.ShellCompDirectiveNoFileComp }) diff --git a/cmd/kubernetes/nodepools/delete.go b/cmd/kubernetes/nodepools/delete.go index 27735ce..4684480 100644 --- a/cmd/kubernetes/nodepools/delete.go +++ b/cmd/kubernetes/nodepools/delete.go @@ -14,7 +14,7 @@ import ( var ( deleteNodePoolCluster string - deleteNodePoolName string + deleteNodePoolId string deleteNodePoolWait bool deleteNodePoolForce bool ) @@ -30,13 +30,13 @@ The cluster must be in a ready state to delete node pools. Examples: # Delete a node pool - tcloud kubernetes nodepools delete --cluster my-cluster --name worker-pool + tcloud kubernetes nodepools delete --cluster my-cluster --nodepool worker-pool # Delete a node pool and wait for completion - tcloud kubernetes nodepools delete --cluster my-cluster --name worker-pool --wait + tcloud kubernetes nodepools delete --cluster my-cluster --nodepool worker-pool --wait # Delete a node pool without confirmation - tcloud kubernetes nodepools delete --cluster my-cluster --name worker-pool --force`, + tcloud kubernetes nodepools delete --cluster my-cluster --nodepool worker-pool --force`, Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() @@ -44,12 +44,12 @@ Examples: if deleteNodePoolCluster == "" { return fmt.Errorf("--cluster is required") } - if deleteNodePoolName == "" { - return fmt.Errorf("--name is required") + if deleteNodePoolId == "" { + return fmt.Errorf("--nodepool is required") } clusterIdentifier := deleteNodePoolCluster - nodePoolIdentifier := deleteNodePoolName + nodePoolIdentifier := deleteNodePoolId client, err := thalassaclient.GetThalassaClient() if err != nil { @@ -138,9 +138,10 @@ func init() { // Command is registered in kubernetesclusters.go deleteCmd.Flags().StringVar(&deleteNodePoolCluster, "cluster", "", "Cluster identity, name, or slug (required)") - deleteCmd.Flags().StringVar(&deleteNodePoolName, "name", "", "Node pool name, identity, or slug (required)") + deleteCmd.Flags().StringVar(&deleteNodePoolId, "nodepool", "", "Node pool name, identity, or slug (required)") deleteCmd.Flags().BoolVar(&deleteNodePoolWait, "wait", false, "Wait for the node pool to be deleted before returning") deleteCmd.Flags().BoolVar(&deleteNodePoolForce, "force", false, "Skip confirmation prompt") - deleteCmd.RegisterFlagCompletionFunc("cluster", completion.CompleteKubernetesCluster) + deleteCmd.RegisterFlagCompletionFunc(ClusterFlag, completion.CompleteKubernetesCluster) + deleteCmd.RegisterFlagCompletionFunc("nodepool", completion.CompleteKubernetesNodePool) } diff --git a/cmd/kubernetes/nodepools/list.go b/cmd/kubernetes/nodepools/list.go index b858a9c..4de83f5 100644 --- a/cmd/kubernetes/nodepools/list.go +++ b/cmd/kubernetes/nodepools/list.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -134,4 +135,8 @@ func init() { listCmd.Flags().BoolVar(&noHeader, NoHeaderKey, false, "Do not print the header") listCmd.Flags().StringVar(&cluster, ClusterFlag, "", "Cluster ID") listCmd.Flags().StringVar(&vpc, VpcFlag, "", "VPC ID") + + // Register completions + listCmd.RegisterFlagCompletionFunc(ClusterFlag, completion.CompleteKubernetesCluster) + listCmd.RegisterFlagCompletionFunc(VpcFlag, completion.CompleteVPCID) } From c5b622b6530df151b7a5b0b3b77205b18543c1fc Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:10:54 +0100 Subject: [PATCH 06/14] feat(kubernetes): add VPC filter support to list command with flag completion --- cmd/kubernetes/list.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/cmd/kubernetes/list.go b/cmd/kubernetes/list.go index 5dcda65..98fa22d 100644 --- a/cmd/kubernetes/list.go +++ b/cmd/kubernetes/list.go @@ -5,18 +5,25 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" + "github.com/thalassa-cloud/client-go/filters" "github.com/thalassa-cloud/client-go/kubernetes" ) const NoHeaderKey = "no-header" +const ( + VpcFlag = "vpc" +) + var noHeader bool var ( showExactTime bool + vpc string ) // listCmd represents the get command @@ -31,7 +38,16 @@ var listCmd = &cobra.Command{ if err != nil { return fmt.Errorf("failed to create client: %w", err) } - clusters, err := client.Kubernetes().ListKubernetesClusters(cmd.Context(), &kubernetes.ListKubernetesClustersRequest{}) + f := []filters.Filter{} + if vpc != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "vpc", + Value: vpc, + }) + } + clusters, err := client.Kubernetes().ListKubernetesClusters(cmd.Context(), &kubernetes.ListKubernetesClustersRequest{ + Filters: f, + }) if err != nil { return err } @@ -68,5 +84,9 @@ var listCmd = &cobra.Command{ func init() { KubernetesCmd.AddCommand(listCmd) + + // flags + listCmd.Flags().StringVar(&vpc, VpcFlag, "", "VPC ID") listCmd.Flags().BoolVar(&noHeader, NoHeaderKey, false, "Do not print the header") + listCmd.RegisterFlagCompletionFunc(VpcFlag, completion.CompleteVPCID) } From 97f228cb416655d869ce667a974ce078fa3186ec Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:11:05 +0100 Subject: [PATCH 07/14] feat(tfs): add region, VPC, and status filters to list command with flag completions --- cmd/iaas/storage/tfs/create.go | 5 +++++ cmd/iaas/storage/tfs/list.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/cmd/iaas/storage/tfs/create.go b/cmd/iaas/storage/tfs/create.go index 07f71ec..3f444b3 100644 --- a/cmd/iaas/storage/tfs/create.go +++ b/cmd/iaas/storage/tfs/create.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -171,6 +172,10 @@ func init() { createCmd.Flags().BoolVar(&createTfsDeleteProtection, "delete-protection", false, "Enable delete protection") createCmd.Flags().BoolVar(&createTfsWait, "wait", false, "Wait for the TFS instance to be available before returning") + // Register completions + createCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) + createCmd.RegisterFlagCompletionFunc("subnet", completion.CompleteSubnetEnhanced) + _ = createCmd.MarkFlagRequired("name") _ = createCmd.MarkFlagRequired("region") } diff --git a/cmd/iaas/storage/tfs/list.go b/cmd/iaas/storage/tfs/list.go index 5544dca..4f7c314 100644 --- a/cmd/iaas/storage/tfs/list.go +++ b/cmd/iaas/storage/tfs/list.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/labels" "github.com/thalassa-cloud/cli/internal/table" @@ -23,6 +24,9 @@ var ( showExactTime bool showLabels bool listLabelSelector string + listRegionFilter string + listVpcFilter string + listStatusFilter string ) // listCmd represents the list command @@ -43,6 +47,24 @@ var listCmd = &cobra.Command{ MatchLabels: labels.ParseLabelSelector(listLabelSelector), }) } + if listRegionFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "region", + Value: listRegionFilter, + }) + } + if listVpcFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "vpc", + Value: listVpcFilter, + }) + } + if listStatusFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "status", + Value: listStatusFilter, + }) + } instances, err := client.Tfs().ListTfsInstances(cmd.Context(), &tfs.ListTfsInstancesRequest{ Filters: f, @@ -101,4 +123,11 @@ func init() { listCmd.Flags().BoolVar(&showExactTime, "exact-time", false, "Show exact time instead of relative time") listCmd.Flags().BoolVar(&showLabels, "show-labels", false, "Show labels") listCmd.Flags().StringVarP(&listLabelSelector, "selector", "l", "", "Label selector to filter TFS instances (format: key1=value1,key2=value2)") + listCmd.Flags().StringVar(&listRegionFilter, "region", "", "Region of the TFS instance") + listCmd.Flags().StringVar(&listVpcFilter, "vpc", "", "VPC of the TFS instance") + listCmd.Flags().StringVar(&listStatusFilter, "status", "", "Status of the TFS instance") + + // Register completions + listCmd.RegisterFlagCompletionFunc("region", completion.CompleteRegion) + listCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) } From be5662fea7ecabe6e0cdadc9b307ee771fd13300 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:11:13 +0100 Subject: [PATCH 08/14] feat(snapshots): add region, status, and volume filters to list command with flag completions --- cmd/iaas/storage/snapshots/list.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/cmd/iaas/storage/snapshots/list.go b/cmd/iaas/storage/snapshots/list.go index c4f500c..c99971e 100644 --- a/cmd/iaas/storage/snapshots/list.go +++ b/cmd/iaas/storage/snapshots/list.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/labels" "github.com/thalassa-cloud/cli/internal/table" @@ -22,6 +23,9 @@ var ( showExactTime bool showLabels bool listLabelSelector string + listRegionFilter string + listStatusFilter string + listVolumeFilter string ) // getCmd represents the get command @@ -43,6 +47,24 @@ var listCmd = &cobra.Command{ MatchLabels: labels.ParseLabelSelector(listLabelSelector), }) } + if listRegionFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "region", + Value: listRegionFilter, + }) + } + if listStatusFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "status", + Value: listStatusFilter, + }) + } + if listVolumeFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "volume", + Value: listVolumeFilter, + }) + } snapshots, err := client.IaaS().ListSnapshots(cmd.Context(), &iaas.ListSnapshotsRequest{ Filters: f, @@ -95,4 +117,11 @@ func init() { listCmd.Flags().BoolVar(&noHeader, NoHeaderKey, false, "Do not print the header") listCmd.Flags().BoolVar(&showLabels, "show-labels", false, "Show labels") listCmd.Flags().StringVarP(&listLabelSelector, "selector", "l", "", "Label selector to filter snapshots (format: key1=value1,key2=value2)") + listCmd.Flags().StringVar(&listRegionFilter, "region", "", "Region of the snapshot") + listCmd.Flags().StringVar(&listStatusFilter, "status", "", "Status of the snapshot") + listCmd.Flags().StringVar(&listVolumeFilter, "volume", "", "Source volume of the snapshot") + + // Register completions + listCmd.RegisterFlagCompletionFunc("region", completion.CompleteRegion) + listCmd.RegisterFlagCompletionFunc("volume", completion.CompleteVolumeID) } From 4d6d801510f44a1e742a39228fa3f423021b9cf9 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:11:39 +0100 Subject: [PATCH 09/14] feat(vpcs): add wait flag to VPC creation command --- cmd/iaas/networking/vpcs/create.go | 40 +++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/cmd/iaas/networking/vpcs/create.go b/cmd/iaas/networking/vpcs/create.go index b61739a..34f2550 100644 --- a/cmd/iaas/networking/vpcs/create.go +++ b/cmd/iaas/networking/vpcs/create.go @@ -1,12 +1,15 @@ package vpcs import ( + "context" "fmt" "strings" + "time" "github.com/spf13/cobra" "github.com/thalassa-cloud/cli/internal/formattime" + iaasutil "github.com/thalassa-cloud/cli/internal/iaas" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -25,6 +28,7 @@ const ( var ( createVpcValues = iaas.CreateVpc{} + createVpcWait bool ) // getCmd represents the get command @@ -50,7 +54,11 @@ var createCmd = &cobra.Command{ return fmt.Errorf("cidrs is required") } - region, err := client.IaaS().GetRegion(cmd.Context(), createVpcValues.CloudRegionIdentity) + regions, err := client.IaaS().ListRegions(cmd.Context(), &iaas.ListRegionsRequest{}) + if err != nil { + return err + } + region, err := iaasutil.FindRegionByIdentitySlugOrNameWithError(regions, createVpcValues.CloudRegionIdentity) if err != nil { return err } @@ -60,6 +68,35 @@ var createCmd = &cobra.Command{ if err != nil { return err } + + if createVpcWait { + ctxWithTimeout, cancel := context.WithTimeout(cmd.Context(), 10*time.Minute) + defer cancel() + + fmt.Println("Waiting for VPC to be ready...") + for { + vpc, err = client.IaaS().GetVpc(ctxWithTimeout, vpc.Identity) + if err != nil { + return fmt.Errorf("failed to get vpc: %w", err) + } + // VPC is ready when status is "ready" or "available" + if strings.EqualFold(vpc.Status, "ready") || strings.EqualFold(vpc.Status, "available") { + break + } + // Check for failed state + if strings.EqualFold(vpc.Status, "failed") || strings.EqualFold(vpc.Status, "error") { + return fmt.Errorf("vpc creation failed with status: %s", vpc.Status) + } + select { + case <-ctxWithTimeout.Done(): + return fmt.Errorf("timeout waiting for vpc %s to be ready (current status: %s)", vpc.Identity, vpc.Status) + case <-time.After(2 * time.Second): + // Continue polling + } + } + fmt.Println("VPC is ready") + } + body := make([][]string, 0, 1) body = append(body, []string{ vpc.Identity, @@ -85,6 +122,7 @@ func init() { createCmd.Flags().StringVar(&createVpcValues.Description, CreateFlagDescription, "", "Description of the vpc") createCmd.Flags().StringVar(&createVpcValues.CloudRegionIdentity, CreateFlagRegion, "", "Region of the vpc") createCmd.Flags().StringSliceVar(&createVpcValues.VpcCidrs, CreateFlagCIDRs, []string{"10.0.0.0/16"}, "CIDRs of the vpc") + createCmd.Flags().BoolVar(&createVpcWait, "wait", false, "Wait for the VPC to be ready before returning") // createCmd.Flags().StringSliceVar(&createVpcValues.Labels, CreateFlagLabels, []string{}, "Labels of the vpc") // createCmd.Flags().StringSliceVar(&createVpcValues.Annotations, CreateFlagAnnotations, []string{}, "Annotations of the vpc") } From d0987e4fe1f3aa554f7ac830562f0903e14f4974 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:11:58 +0100 Subject: [PATCH 10/14] feat(vpcpeering): add flag completions for requester and accepter VPCs --- cmd/iaas/networking/vpcpeering/create.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cmd/iaas/networking/vpcpeering/create.go b/cmd/iaas/networking/vpcpeering/create.go index 010f86c..00cb968 100644 --- a/cmd/iaas/networking/vpcpeering/create.go +++ b/cmd/iaas/networking/vpcpeering/create.go @@ -6,6 +6,7 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -166,6 +167,10 @@ func init() { createCmd.Flags().StringSliceVar(&createLabels, CreateFlagLabels, []string{}, "Labels in key=value format") createCmd.Flags().StringSliceVar(&createAnnotations, CreateFlagAnnotations, []string{}, "Annotations in key=value format") + // Register completions + createCmd.RegisterFlagCompletionFunc("requester-vpc", completion.CompleteVPCID) + createCmd.RegisterFlagCompletionFunc("accepter-vpc", completion.CompleteVPCID) + createCmd.MarkFlagRequired(CreateFlagName) createCmd.MarkFlagRequired(CreateFlagRequesterVpc) createCmd.MarkFlagRequired(CreateFlagAccepterVpc) From 3434a8de7addf5a2b6e584237a2da9c06a2f3eab Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:12:14 +0100 Subject: [PATCH 11/14] feat(subnets): add wait flag to subnet creation command --- cmd/iaas/networking/subnets/create.go | 62 ++++++++++++++++++--------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/cmd/iaas/networking/subnets/create.go b/cmd/iaas/networking/subnets/create.go index c16901c..56b8b7f 100644 --- a/cmd/iaas/networking/subnets/create.go +++ b/cmd/iaas/networking/subnets/create.go @@ -1,14 +1,18 @@ package subnets import ( + "context" "fmt" + "strings" + "time" "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" + iaasutil "github.com/thalassa-cloud/cli/internal/iaas" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" "github.com/thalassa-cloud/client-go/iaas" - "github.com/thalassa-cloud/client-go/pkg/client" ) const ( @@ -23,6 +27,7 @@ const ( var ( createSubnetValues = iaas.CreateSubnet{} + createSubnetWait bool ) // getCmd represents the get command @@ -46,26 +51,9 @@ var createCmd = &cobra.Command{ return fmt.Errorf("cidr is required") } - vpc, err := tcclient.IaaS().GetVpc(cmd.Context(), createSubnetValues.VpcIdentity) + vpc, err := iaasutil.GetVPCByIdentitySlugOrName(cmd.Context(), tcclient.IaaS(), createSubnetValues.VpcIdentity) if err != nil { - if client.IsNotFound(err) { - vpcs, err := tcclient.IaaS().ListVpcs(cmd.Context(), &iaas.ListVpcsRequest{}) - if err != nil { - return err - } - for _, v := range vpcs { - if v.Slug == createSubnetValues.VpcIdentity { - createSubnetValues.VpcIdentity = v.Identity - vpc = &v - break - } - } - if vpc == nil { - return fmt.Errorf("vpc not found") - } - } else { - return err - } + return err } createSubnetValues.VpcIdentity = vpc.Identity @@ -73,6 +61,36 @@ var createCmd = &cobra.Command{ if err != nil { return err } + + if createSubnetWait { + ctxWithTimeout, cancel := context.WithTimeout(cmd.Context(), 10*time.Minute) + defer cancel() + + fmt.Println("Waiting for subnet to be ready...") + for { + subnet, err = tcclient.IaaS().GetSubnet(ctxWithTimeout, subnet.Identity) + if err != nil { + return fmt.Errorf("failed to get subnet: %w", err) + } + // Subnet is ready when status is "ready" or "available" + status := string(subnet.Status) + if strings.EqualFold(status, "ready") || strings.EqualFold(status, "available") { + break + } + // Check for failed state + if strings.EqualFold(status, "failed") || strings.EqualFold(status, "error") { + return fmt.Errorf("subnet creation failed with status: %s", status) + } + select { + case <-ctxWithTimeout.Done(): + return fmt.Errorf("timeout waiting for subnet %s to be ready (current status: %s)", subnet.Identity, status) + case <-time.After(2 * time.Second): + // Continue polling + } + } + fmt.Println("Subnet is ready") + } + body := make([][]string, 0, 1) body = append(body, []string{ subnet.Identity, @@ -97,4 +115,8 @@ func init() { createCmd.Flags().StringVar(&createSubnetValues.Description, CreateFlagDescription, "", "Description of the subnet") createCmd.Flags().StringVar(&createSubnetValues.VpcIdentity, CreateFlagVpc, "", "VPC of the subnet") createCmd.Flags().StringVar(&createSubnetValues.Cidr, CreateFlagCIDR, "", "CIDR of the subnet") + createCmd.Flags().BoolVar(&createSubnetWait, "wait", false, "Wait for the subnet to be ready before returning") + + // Register completions + createCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) } From cdf537deecaf296e7cc61372c00b52c9f1f40c80 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:12:29 +0100 Subject: [PATCH 12/14] fix(machines): allow up to one argument for start and stop commands --- cmd/iaas/compute/machines/machines.go | 5 ----- cmd/iaas/compute/machines/start.go | 2 +- cmd/iaas/compute/machines/stop.go | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/iaas/compute/machines/machines.go b/cmd/iaas/compute/machines/machines.go index 638fe97..7b519b7 100644 --- a/cmd/iaas/compute/machines/machines.go +++ b/cmd/iaas/compute/machines/machines.go @@ -7,7 +7,6 @@ import ( "github.com/spf13/cobra" - "github.com/thalassa-cloud/cli/internal/config/contextstate" "github.com/thalassa-cloud/cli/internal/fzf" ) @@ -23,10 +22,6 @@ func init() { } func getSelectedMachine(args []string) (string, error) { - if contextstate.OrganisationFlag != "" { - return contextstate.OrganisationFlag, nil - } - if len(args) == 0 && fzf.IsInteractiveMode(os.Stdout) { command := fmt.Sprintf("%s compute machines ls --no-header", os.Args[0]) return fzf.InteractiveChoice(command) diff --git a/cmd/iaas/compute/machines/start.go b/cmd/iaas/compute/machines/start.go index ee4818f..23940d5 100644 --- a/cmd/iaas/compute/machines/start.go +++ b/cmd/iaas/compute/machines/start.go @@ -16,7 +16,7 @@ var startCmd = &cobra.Command{ Short: "Start a machine", Long: "Start a machine to start it from stopped state. This command will start the machine and all the services associated with it.", Aliases: []string{"s", "start"}, - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := thalassaclient.GetThalassaClient() diff --git a/cmd/iaas/compute/machines/stop.go b/cmd/iaas/compute/machines/stop.go index fa3c17f..c21c3a1 100644 --- a/cmd/iaas/compute/machines/stop.go +++ b/cmd/iaas/compute/machines/stop.go @@ -20,7 +20,7 @@ var stopCmd = &cobra.Command{ Short: "Stop a machine", Long: "Stop a machine to stop it from running. This command will stop the machine and all the services associated with it.", Aliases: []string{"s", "stop"}, - Args: cobra.NoArgs, + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { client, err := thalassaclient.GetThalassaClient() From 1bd7459fbc464fecce7b56c91e462f81e7cb7442 Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:12:41 +0100 Subject: [PATCH 13/14] feat(machines): list command with region, VPC, and status filters, and add flag completions --- cmd/iaas/compute/machines/delete.go | 2 +- cmd/iaas/compute/machines/list.go | 35 ++++++++++++++++++++++++++--- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/cmd/iaas/compute/machines/delete.go b/cmd/iaas/compute/machines/delete.go index 54f6f40..b58abe8 100644 --- a/cmd/iaas/compute/machines/delete.go +++ b/cmd/iaas/compute/machines/delete.go @@ -25,7 +25,7 @@ var deleteCmd = &cobra.Command{ Long: "Delete machine(s) by identity or label selector. This command will delete the machine(s) and all the services associated with it.", Example: "tcloud compute machines delete vm-123\ntcloud compute machines delete vm-123 vm-456 --wait\ntcloud compute machines delete --selector environment=test --force", Aliases: []string{"d", "del", "remove"}, - Args: cobra.MinimumNArgs(0), + Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 && labelSelector == "" { // Try interactive selection if no args and no selector diff --git a/cmd/iaas/compute/machines/list.go b/cmd/iaas/compute/machines/list.go index 592da12..310d45a 100644 --- a/cmd/iaas/compute/machines/list.go +++ b/cmd/iaas/compute/machines/list.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" "github.com/thalassa-cloud/cli/internal/labels" "github.com/thalassa-cloud/cli/internal/table" @@ -22,10 +23,13 @@ const NoHeaderKey = "no-header" var noHeader bool var ( - showExactTime bool - showLabels bool + showExactTime bool + showLabels bool listLabelSelector string - outputFormat string + outputFormat string + listRegionFilter string + listVpcFilter string + listStatusFilter string ) // getCmd represents the get command @@ -47,6 +51,24 @@ var getCmd = &cobra.Command{ MatchLabels: labels.ParseLabelSelector(listLabelSelector), }) } + if listRegionFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "region", + Value: listRegionFilter, + }) + } + if listVpcFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "vpc", + Value: listVpcFilter, + }) + } + if listStatusFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "status", + Value: listStatusFilter, + }) + } machines, err := client.IaaS().ListMachines(cmd.Context(), &iaas.ListMachinesRequest{ Filters: f, @@ -149,4 +171,11 @@ func init() { getCmd.Flags().BoolVar(&showLabels, "show-labels", false, "Show labels associated with machines") getCmd.Flags().StringVarP(&listLabelSelector, "selector", "l", "", "Label selector to filter machines (format: key1=value1,key2=value2)") getCmd.Flags().StringVarP(&outputFormat, "output", "o", "", "Output format. One of: wide") + getCmd.Flags().StringVar(&listRegionFilter, "region", "", "Region of the machine") + getCmd.Flags().StringVar(&listVpcFilter, "vpc", "", "VPC of the machine") + getCmd.Flags().StringVar(&listStatusFilter, "status", "", "Status of the machine") + + // Register completions + getCmd.RegisterFlagCompletionFunc("region", completion.CompleteRegion) + getCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) } From eb5fe766149263ba022613479f422104fa2c2c5c Mon Sep 17 00:00:00 2001 From: Thomas Kooi Date: Sun, 25 Jan 2026 01:12:54 +0100 Subject: [PATCH 14/14] feat(dbaas): improved create and list commands with VPC, subnet, and volume type support, and add flag completions --- cmd/dbaas/create.go | 55 ++++++++++++++++++++++++++++++++------ cmd/dbaas/instancetypes.go | 4 +-- cmd/dbaas/list.go | 54 ++++++++++++++++++++++++++++++++++++- cmd/dbaas/update.go | 3 +++ 4 files changed, 105 insertions(+), 11 deletions(-) diff --git a/cmd/dbaas/create.go b/cmd/dbaas/create.go index 52604e0..50e61a0 100644 --- a/cmd/dbaas/create.go +++ b/cmd/dbaas/create.go @@ -7,10 +7,13 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" + iaasutil "github.com/thalassa-cloud/cli/internal/iaas" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" "github.com/thalassa-cloud/client-go/dbaas" + "github.com/thalassa-cloud/client-go/iaas" ) var ( @@ -19,8 +22,10 @@ var ( createClusterEngine string createClusterEngineVersion string createClusterInstanceType string + createClusterVpc string createClusterSubnet string createClusterStorage int + createclusterVolumeType string createClusterReplicas int createClusterLabels []string createClusterAnnotations []string @@ -59,14 +64,38 @@ var createCmd = &cobra.Command{ return fmt.Errorf("replicas must be 0 or greater") } - // Resolve subnet if provided - var subnetIdentity string - if createClusterSubnet != "" { - subnet, err := client.IaaS().GetSubnet(cmd.Context(), createClusterSubnet) + // Resolve volume type + volumeTypes, err := client.IaaS().ListVolumeTypes(cmd.Context(), &iaas.ListVolumeTypesRequest{}) + if err != nil { + return fmt.Errorf("failed to get volume type: %w", err) + } + volumeType, err := iaasutil.FindVolumeTypeByIdentitySlugOrNameWithError(volumeTypes, createclusterVolumeType) + if err != nil { + return err + } + + createclusterVolumeType = volumeType.Identity + + // Resolve subnet (required) + if createClusterSubnet == "" { + return fmt.Errorf("subnet is required") + } + subnet, err := iaasutil.GetSubnetByIdentitySlugOrName(cmd.Context(), client.IaaS(), createClusterSubnet) + if err != nil { + return fmt.Errorf("failed to get subnet: %w", err) + } + subnetIdentity := subnet.Identity + + // Resolve and validate VPC if provided + if createClusterVpc != "" { + vpc, err := iaasutil.GetVPCByIdentitySlugOrName(cmd.Context(), client.IaaS(), createClusterVpc) if err != nil { - return fmt.Errorf("failed to get subnet: %w", err) + return fmt.Errorf("failed to get vpc: %w", err) + } + // Validate that the subnet belongs to the specified VPC + if subnet.Vpc != nil && subnet.Vpc.Identity != vpc.Identity { + return fmt.Errorf("subnet %s does not belong to VPC %s", subnetIdentity, vpc.Identity) } - subnetIdentity = subnet.Identity } // Parse labels from key=value format @@ -93,6 +122,7 @@ var createCmd = &cobra.Command{ Engine: dbaas.DbClusterDatabaseEngine(createClusterEngine), EngineVersion: createClusterEngineVersion, DatabaseInstanceTypeIdentity: createClusterInstanceType, + VolumeTypeClassIdentity: createclusterVolumeType, SubnetIdentity: subnetIdentity, AllocatedStorage: uint64(createClusterStorage), Labels: labels, @@ -182,7 +212,10 @@ func init() { createCmd.Flags().StringVar(&createClusterEngine, "engine", "", "Database engine (e.g., postgres) (required)") createCmd.Flags().StringVar(&createClusterEngineVersion, "engine-version", "", "Engine version (required)") createCmd.Flags().StringVar(&createClusterInstanceType, "instance-type", "", "Instance type (required)") - createCmd.Flags().StringVar(&createClusterSubnet, "subnet", "", "Subnet identity") + + createCmd.Flags().StringVar(&createClusterVpc, "vpc", "", "VPC identity, slug, or name") + createCmd.Flags().StringVar(&createClusterSubnet, "subnet", "", "Subnet identity, slug, or name (required)") + createCmd.Flags().StringVar(&createclusterVolumeType, "volume-type", "block", "Volume type") createCmd.Flags().IntVar(&createClusterStorage, "storage", 0, "Storage size in GB (required)") createCmd.Flags().IntVar(&createClusterReplicas, "replicas", 0, "Number of replicas (default: 0)") createCmd.Flags().StringSliceVar(&createClusterLabels, "labels", []string{}, "Labels in key=value format (can be specified multiple times)") @@ -190,10 +223,16 @@ func init() { createCmd.Flags().BoolVar(&createClusterDeleteProtection, "delete-protection", false, "Enable delete protection") createCmd.Flags().BoolVar(&createClusterWait, "wait", false, "Wait for the database cluster to be available before returning") + // Register completions + createCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) + createCmd.RegisterFlagCompletionFunc("subnet", completion.CompleteSubnetEnhanced) + createCmd.RegisterFlagCompletionFunc("engine-version", completion.CompleteDbEngineVersion) + _ = createCmd.MarkFlagRequired("name") _ = createCmd.MarkFlagRequired("engine") _ = createCmd.MarkFlagRequired("engine-version") _ = createCmd.MarkFlagRequired("instance-type") - _ = createCmd.MarkFlagRequired("vpc") + _ = createCmd.MarkFlagRequired("subnet") + _ = createCmd.MarkFlagRequired("volume-type") _ = createCmd.MarkFlagRequired("storage") } diff --git a/cmd/dbaas/instancetypes.go b/cmd/dbaas/instancetypes.go index 10e0550..e6fc24e 100644 --- a/cmd/dbaas/instancetypes.go +++ b/cmd/dbaas/instancetypes.go @@ -42,9 +42,9 @@ var instanceTypesCmd = &cobra.Command{ body = append(body, []string{ instanceType.Identity, instanceType.Name, + instanceType.CategorySlug, fmt.Sprintf("%d vCPU", instanceType.Cpus), fmt.Sprintf("%d GB", instanceType.Memory), - instanceType.Description, instanceType.Architecture, }) } @@ -56,7 +56,7 @@ var instanceTypesCmd = &cobra.Command{ if noHeader { table.Print(nil, body) } else { - table.Print([]string{"ID", "Name", "vCPU", "Memory", "Description", "Architecture"}, body) + table.Print([]string{"ID", "Name", "Category", "vCPU", "Memory", "Architecture"}, body) } return nil }, diff --git a/cmd/dbaas/list.go b/cmd/dbaas/list.go index 0b55ca3..62a988d 100644 --- a/cmd/dbaas/list.go +++ b/cmd/dbaas/list.go @@ -7,7 +7,9 @@ import ( "github.com/spf13/cobra" + "github.com/thalassa-cloud/cli/internal/completion" "github.com/thalassa-cloud/cli/internal/formattime" + iaasutil "github.com/thalassa-cloud/cli/internal/iaas" "github.com/thalassa-cloud/cli/internal/labels" "github.com/thalassa-cloud/cli/internal/table" "github.com/thalassa-cloud/cli/internal/thalassaclient" @@ -23,6 +25,9 @@ var ( showExactTime bool showLabels bool listLabelSelector string + listEngineFilter string + listVpcFilter string + listSubnetFilter string ) // listCmd represents the list command @@ -40,6 +45,40 @@ var listCmd = &cobra.Command{ } f := []filters.Filter{} + + // Resolve VPC filter if provided + if listVpcFilter != "" { + vpc, err := iaasutil.GetVPCByIdentitySlugOrName(cmd.Context(), client.IaaS(), listVpcFilter) + if err != nil { + return fmt.Errorf("failed to get vpc: %w", err) + } + f = append(f, &filters.FilterKeyValue{ + Key: "vpc", + Value: vpc.Identity, + }) + } + + // Resolve subnet filter if provided + if listSubnetFilter != "" { + subnet, err := iaasutil.GetSubnetByIdentitySlugOrName(cmd.Context(), client.IaaS(), listSubnetFilter) + if err != nil { + return fmt.Errorf("failed to get subnet: %w", err) + } + f = append(f, &filters.FilterKeyValue{ + Key: "subnet", + Value: subnet.Identity, + }) + } + + // Add engine filter if provided + if listEngineFilter != "" { + f = append(f, &filters.FilterKeyValue{ + Key: "engine", + Value: listEngineFilter, + }) + } + + // Add label selector filter if provided if listLabelSelector != "" { f = append(f, &filters.LabelFilter{ MatchLabels: labels.ParseLabelSelector(listLabelSelector), @@ -60,6 +99,11 @@ var listCmd = &cobra.Command{ vpcName = cluster.Vpc.Name } + subnetName := "" + if cluster.Subnet != nil { + subnetName = cluster.Subnet.Name + } + engineVersion := cluster.EngineVersion if cluster.DatabaseEngineVersion != nil { engineVersion = cluster.DatabaseEngineVersion.EngineVersion @@ -74,6 +118,7 @@ var listCmd = &cobra.Command{ cluster.Identity, cluster.Name, vpcName, + subnetName, string(cluster.Engine), engineVersion, instanceType, @@ -105,7 +150,7 @@ var listCmd = &cobra.Command{ if noHeader { table.Print(nil, body) } else { - headers := []string{"ID", "Name", "VPC", "Engine", "Version", "Instance Type", "Replicas", "Storage", "Status", "Age"} + headers := []string{"ID", "Name", "VPC", "Subnet", "Engine", "Version", "Instance Type", "Replicas", "Storage", "Status", "Age"} if showLabels { headers = append(headers, "Labels") } @@ -121,4 +166,11 @@ func init() { listCmd.Flags().BoolVar(&showExactTime, "exact-time", false, "Show exact time instead of relative time") listCmd.Flags().BoolVar(&showLabels, "show-labels", false, "Show labels") listCmd.Flags().StringVarP(&listLabelSelector, "selector", "l", "", "Label selector to filter clusters (format: key1=value1,key2=value2)") + listCmd.Flags().StringVar(&listEngineFilter, "engine", "", "Filter by database engine (e.g., postgres)") + listCmd.Flags().StringVar(&listVpcFilter, "vpc", "", "Filter by VPC identity, slug, or name") + listCmd.Flags().StringVar(&listSubnetFilter, "subnet", "", "Filter by subnet identity, slug, or name") + + // Register completions + listCmd.RegisterFlagCompletionFunc("vpc", completion.CompleteVPCID) + listCmd.RegisterFlagCompletionFunc("subnet", completion.CompleteSubnetEnhanced) } diff --git a/cmd/dbaas/update.go b/cmd/dbaas/update.go index 75af41b..4d105d8 100644 --- a/cmd/dbaas/update.go +++ b/cmd/dbaas/update.go @@ -174,4 +174,7 @@ func init() { updateCmd.Flags().StringSliceVar(&updateClusterLabels, "labels", []string{}, "Labels in key=value format (can be specified multiple times)") updateCmd.Flags().StringSliceVar(&updateClusterAnnotations, "annotations", []string{}, "Annotations in key=value format (can be specified multiple times)") updateCmd.Flags().BoolVar(&updateClusterDeleteProtection, "delete-protection", false, "Enable or disable delete protection") + + // Register completions + updateCmd.RegisterFlagCompletionFunc("instance-type", completion.CompleteDbInstanceType) }