From 42a334d71056ad830d76a666bdfce75f94971f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 21:29:11 +0300 Subject: [PATCH 1/8] fix: add short ID resolution to remaining SDK methods (#475, #473, #471, #470, #453) - snapshot CreateSnapshot: now accepts volume ID or name - autoscaling CreateScalingPolicy: now accepts group ID or name - compute: added resolveID to GetInstance, StopInstance, TerminateInstance, GetInstanceLogs, ResizeInstance, GetInstanceStats, GetConsoleURL, UpdateInstanceMetadata - kubernetes: added resolveID to all cluster methods (GetCluster, DeleteCluster, GetKubeconfig, RepairCluster, ScaleCluster, GetClusterHealth, UpgradeCluster, RotateSecrets, CreateBackup, RestoreBackup, AddNodeGroup, UpdateNodeGroupWithContext, DeleteNodeGroup) - dns CreateDNSZone: now accepts VPC ID or name Updated tests to handle the list-then-get resolution pattern in mocks --- cmd/cloud/autoscaling_cli_test.go | 20 +++++ cmd/cloud/dns.go | 36 ++------ cmd/cloud/snapshot.go | 9 +- pkg/sdk/autoscaling.go | 11 ++- pkg/sdk/compute.go | 81 ++++++++++++++++-- pkg/sdk/compute_test.go | 100 ++++++++++++++++++++++- pkg/sdk/dns.go | 13 ++- pkg/sdk/kubernetes.go | 131 +++++++++++++++++++++++++----- pkg/sdk/snapshot.go | 14 +++- pkg/sdk/snapshot_test.go | 14 +++- 10 files changed, 348 insertions(+), 81 deletions(-) diff --git a/cmd/cloud/autoscaling_cli_test.go b/cmd/cloud/autoscaling_cli_test.go index bbe6f1301..1f5839a38 100644 --- a/cmd/cloud/autoscaling_cli_test.go +++ b/cmd/cloud/autoscaling_cli_test.go @@ -165,6 +165,26 @@ func TestASGDeleteCmd(t *testing.T) { func TestASGPolicyAddCmd(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") + if r.URL.Path == "/autoscaling/groups" && r.Method == http.MethodGet { + payload := map[string]interface{}{ + "data": []map[string]interface{}{ + { + "id": asgTestID, + "name": asgTestName, + "vpc_id": "vpc-1", + "image": "nginx:latest", + "min_instances": 1, + "max_instances": 3, + "desired_count": 2, + "current_count": 1, + "status": "active", + "created_at": time.Now().UTC().Format(time.RFC3339), + }, + }, + } + _ = json.NewEncoder(w).Encode(payload) + return + } if r.URL.Path != "/autoscaling/groups/"+asgTestID+"/policies" || r.Method != http.MethodPost { w.WriteHeader(http.StatusNotFound) return diff --git a/cmd/cloud/dns.go b/cmd/cloud/dns.go index ff1ff6fa9..373cc6e56 100644 --- a/cmd/cloud/dns.go +++ b/cmd/cloud/dns.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "strconv" - "strings" "github.com/google/uuid" "github.com/olekukonko/tablewriter" @@ -61,38 +60,15 @@ var dnsCreateZoneCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { name := args[0] desc, _ := cmd.Flags().GetString("description") - vpcStr, _ := cmd.Flags().GetString("vpc-id") - - var vpcID *uuid.UUID - if vpcStr != "" { - // Try to resolve as full UUID first - uid, err := uuid.Parse(vpcStr) - if err != nil { - // Not a UUID, try to resolve by name or short ID - client := createClient(opts) - vpcs, err := client.ListVPCs() - if err == nil { - for _, vpc := range vpcs { - if vpc.Name == vpcStr || strings.HasPrefix(vpc.ID, vpcStr) { - uid, err := uuid.Parse(vpc.ID) - if err == nil { - vpcID = &uid - break - } - } - } - } - if vpcID == nil { - fmt.Printf("Error: invalid vpc-id format: %v\n", err) - return - } - } else { - vpcID = &uid - } + vpcIDOrName, _ := cmd.Flags().GetString("vpc-id") + + var vpcPtr *string + if vpcIDOrName != "" { + vpcPtr = &vpcIDOrName } client := createClient(opts) - zone, err := client.CreateDNSZone(name, desc, vpcID) + zone, err := client.CreateDNSZone(name, desc, vpcPtr) if err != nil { fmt.Printf(dnsErrorFormat, err) return diff --git a/cmd/cloud/snapshot.go b/cmd/cloud/snapshot.go index ff7238de4..5b174d084 100644 --- a/cmd/cloud/snapshot.go +++ b/cmd/cloud/snapshot.go @@ -5,7 +5,6 @@ import ( "fmt" "os" - "github.com/google/uuid" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" ) @@ -64,15 +63,11 @@ var snapshotCreateCmd = &cobra.Command{ Short: "Create a snapshot from a volume", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { - volID, err := uuid.Parse(args[0]) - if err != nil { - fmt.Printf(snapshotErrorFormat, err) - return - } + volIDOrName := args[0] desc, _ := cmd.Flags().GetString("desc") client := createClient(opts) - snapshot, err := client.CreateSnapshot(volID, desc) + snapshot, err := client.CreateSnapshot(volIDOrName, desc) if err != nil { fmt.Printf(snapshotErrorFormat, err) return diff --git a/pkg/sdk/autoscaling.go b/pkg/sdk/autoscaling.go index 340a80333..5c247325b 100644 --- a/pkg/sdk/autoscaling.go +++ b/pkg/sdk/autoscaling.go @@ -117,10 +117,17 @@ type CreatePolicyRequest struct { CooldownSec int `json:"cooldown_sec"` } -func (c *Client) CreateScalingPolicy(groupID string, req CreatePolicyRequest) error { +func (c *Client) CreateScalingPolicy(groupIDOrName string, req CreatePolicyRequest) error { + id, err := c.resolveID("scaling-group", func() ([]interface{}, error) { + groups, err := c.ListScalingGroups() + return interfaceSlice(groups), err + }, func(v interface{}) string { return v.(ScalingGroup).ID }, func(v interface{}) string { return v.(ScalingGroup).Name }, groupIDOrName) + if err != nil { + return err + } resp, err := c.resty.R(). SetBody(req). - Post(fmt.Sprintf("%s/autoscaling/groups/%s/policies", c.apiURL, groupID)) + Post(fmt.Sprintf("%s/autoscaling/groups/%s/policies", c.apiURL, id)) if err != nil { return err diff --git a/pkg/sdk/compute.go b/pkg/sdk/compute.go index 05d5abd67..c7ab2dd2b 100644 --- a/pkg/sdk/compute.go +++ b/pkg/sdk/compute.go @@ -47,8 +47,15 @@ func (c *Client) ListInstancesWithContext(ctx context.Context) ([]Instance, erro // GetInstance retrieves a compute instance by ID or name. func (c *Client) GetInstance(idOrName string) (*Instance, error) { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return nil, err + } var res Response[Instance] - if err := c.get(fmt.Sprintf("/instances/%s", idOrName), &res); err != nil { + if err := c.get(fmt.Sprintf("/instances/%s", id), &res); err != nil { return nil, err } return &res.Data, nil @@ -56,16 +63,30 @@ func (c *Client) GetInstance(idOrName string) (*Instance, error) { // GetInstanceWithContext retrieves a compute instance with context support. func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (*Instance, error) { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return nil, err + } var res Response[Instance] - if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s", idOrName), &res); err != nil { + if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s", id), &res); err != nil { return nil, err } return &res.Data, nil } func (c *Client) GetConsoleURL(idOrName string) (string, error) { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return "", err + } var res Response[string] - if err := c.get(fmt.Sprintf("/instances/%s/console", idOrName), &res); err != nil { + if err := c.get(fmt.Sprintf("/instances/%s/console", id), &res); err != nil { return "", err } return res.Data, nil @@ -100,7 +121,14 @@ func (c *Client) LaunchInstance(name, image, ports, instanceType string, vpcID, } // UpdateInstanceMetadata updates the metadata and labels of an instance. -func (c *Client) UpdateInstanceMetadata(id string, metadata, labels map[string]string) error { +func (c *Client) UpdateInstanceMetadata(idOrName string, metadata, labels map[string]string) error { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return err + } body := map[string]interface{}{ "metadata": metadata, "labels": labels, @@ -110,7 +138,14 @@ func (c *Client) UpdateInstanceMetadata(id string, metadata, labels map[string]s // StopInstance stops a running instance by ID or name. func (c *Client) StopInstance(idOrName string) error { - return c.post(fmt.Sprintf("/instances/%s/stop", idOrName), nil, nil) + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return err + } + return c.post(fmt.Sprintf("/instances/%s/stop", id), nil, nil) } // TerminateInstance deletes an instance by ID or name. @@ -120,12 +155,26 @@ func (c *Client) TerminateInstance(idOrName string) error { // TerminateInstanceWithContext deletes an instance with context support. func (c *Client) TerminateInstanceWithContext(ctx context.Context, idOrName string) error { - return c.deleteWithContext(ctx, fmt.Sprintf("/instances/%s", idOrName), nil) + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return err + } + return c.deleteWithContext(ctx, fmt.Sprintf("/instances/%s", id), nil) } // GetInstanceLogs retrieves the raw log output for an instance. func (c *Client) GetInstanceLogs(idOrName string) (string, error) { - resp, err := c.resty.R().Get(c.apiURL + fmt.Sprintf("/instances/%s/logs", idOrName)) + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return "", err + } + resp, err := c.resty.R().Get(c.apiURL + fmt.Sprintf("/instances/%s/logs", id)) if err != nil { return "", err } @@ -137,10 +186,17 @@ func (c *Client) GetInstanceLogs(idOrName string) (string, error) { // ResizeInstance changes the instance type of a running or stopped instance. func (c *Client) ResizeInstance(idOrName, newInstanceType string) error { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return err + } body := map[string]string{ "instance_type": newInstanceType, } - return c.post(fmt.Sprintf("/instances/%s/resize", idOrName), body, nil) + return c.post(fmt.Sprintf("/instances/%s/resize", id), body, nil) } // InstanceStats captures resource usage for an instance. @@ -153,8 +209,15 @@ type InstanceStats struct { // GetInstanceStats returns resource usage metrics for an instance. func (c *Client) GetInstanceStats(idOrName string) (*InstanceStats, error) { + id, err := c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstances() + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + if err != nil { + return nil, err + } var res Response[InstanceStats] - if err := c.get(fmt.Sprintf("/instances/%s/stats", idOrName), &res); err != nil { + if err := c.get(fmt.Sprintf("/instances/%s/stats", id), &res); err != nil { return nil, err } return &res.Data, nil diff --git a/pkg/sdk/compute_test.go b/pkg/sdk/compute_test.go index 14f1388af..32df954c9 100644 --- a/pkg/sdk/compute_test.go +++ b/pkg/sdk/compute_test.go @@ -55,6 +55,12 @@ func TestClientGetInstance(t *testing.T) { } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID, r.URL.Path) assert.Equal(t, "GET", r.Method) @@ -102,10 +108,21 @@ func TestClientLaunchInstance(t *testing.T) { } func TestClientStopInstance(t *testing.T) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID+"/stop", r.URL.Path) assert.Equal(t, "POST", r.Method) - w.Header().Set(computeContentType, computeApplicationJSON) // Added for consistency, though no body is returned + w.Header().Set(computeContentType, computeApplicationJSON) w.WriteHeader(http.StatusOK) })) defer server.Close() @@ -117,7 +134,18 @@ func TestClientStopInstance(t *testing.T) { } func TestClientTerminateInstance(t *testing.T) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID, r.URL.Path) assert.Equal(t, "DELETE", r.Method) w.WriteHeader(http.StatusOK) @@ -132,8 +160,19 @@ func TestClientTerminateInstance(t *testing.T) { func TestClientGetInstanceLogs(t *testing.T) { mockLogs := "hello world\n" + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID+"/logs", r.URL.Path) w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(mockLogs)) @@ -148,7 +187,18 @@ func TestClientGetInstanceLogs(t *testing.T) { } func TestClientGetInstanceLogsErrorStatus(t *testing.T) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID+"/logs", r.URL.Path) w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("boom")) @@ -174,8 +224,19 @@ func TestClientGetInstanceStats(t *testing.T) { CPUPercentage: 15.5, MemoryUsageBytes: 1024 * 1024 * 10, } + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID+"/stats", r.URL.Path) w.Header().Set(computeContentType, computeApplicationJSON) w.WriteHeader(http.StatusOK) @@ -191,7 +252,18 @@ func TestClientGetInstanceStats(t *testing.T) { } func TestClientComputeErrors(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("boom")) })) @@ -234,7 +306,18 @@ func TestClientAPIError(t *testing.T) { } func TestClientResizeInstance(t *testing.T) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } assert.Equal(t, computeInstancesPath+computeInstanceID+"/resize", r.URL.Path) assert.Equal(t, http.MethodPost, r.Method) @@ -255,7 +338,18 @@ func TestClientResizeInstance(t *testing.T) { } func TestClientResizeInstanceError(t *testing.T) { - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + mockInstance := Instance{ + ID: computeInstanceID, + Name: "test-instance", + Status: "running", + } + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/instances" && r.Method == "GET" { + w.Header().Set(computeContentType, computeApplicationJSON) + w.WriteHeader(http.StatusOK) + _ = json.NewEncoder(w).Encode(Response[[]Instance]{Data: []Instance{mockInstance}}) + return + } w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("boom")) })) diff --git a/pkg/sdk/dns.go b/pkg/sdk/dns.go index 0b6b8c82a..a9d9e6322 100644 --- a/pkg/sdk/dns.go +++ b/pkg/sdk/dns.go @@ -17,13 +17,20 @@ func (c *Client) ListDNSZones() ([]domain.DNSZone, error) { } // CreateDNSZone creates a new DNS zone. -func (c *Client) CreateDNSZone(name, description string, vpcID *uuid.UUID) (*domain.DNSZone, error) { +func (c *Client) CreateDNSZone(name, description string, vpcIDOrName *string) (*domain.DNSZone, error) { payload := map[string]interface{}{ "name": name, "description": description, } - if vpcID != nil { - payload["vpc_id"] = vpcID.String() + if vpcIDOrName != nil { + id, err := c.resolveID("vpc", func() ([]interface{}, error) { + vpcs, err := c.ListVPCs() + return interfaceSlice(vpcs), err + }, func(v interface{}) string { return v.(VPC).ID }, func(v interface{}) string { return v.(VPC).Name }, *vpcIDOrName) + if err != nil { + return nil, err + } + payload["vpc_id"] = id } var resp struct { diff --git a/pkg/sdk/kubernetes.go b/pkg/sdk/kubernetes.go index a429ee3dc..f61b0aa21 100644 --- a/pkg/sdk/kubernetes.go +++ b/pkg/sdk/kubernetes.go @@ -99,13 +99,20 @@ func (c *Client) CreateCluster(input *CreateClusterInput) (*Cluster, error) { return resp.Data, nil } -// GetCluster retrieves cluster details by ID. -func (c *Client) GetCluster(id string) (*Cluster, error) { - return c.GetClusterWithContext(context.Background(), id) +// GetCluster retrieves cluster details by ID or name. +func (c *Client) GetCluster(idOrName string) (*Cluster, error) { + return c.GetClusterWithContext(context.Background(), idOrName) } -// GetClusterWithContext retrieves cluster details by ID with context support. -func (c *Client) GetClusterWithContext(ctx context.Context, id string) (*Cluster, error) { +// GetClusterWithContext retrieves cluster details by ID or name with context support. +func (c *Client) GetClusterWithContext(ctx context.Context, idOrName string) (*Cluster, error) { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return nil, err + } var resp Response[*Cluster] if err := c.getWithContext(ctx, clustersPath+"/"+id, &resp); err != nil { return nil, err @@ -114,13 +121,27 @@ func (c *Client) GetClusterWithContext(ctx context.Context, id string) (*Cluster } // DeleteCluster removes a cluster. -func (c *Client) DeleteCluster(id string) error { +func (c *Client) DeleteCluster(idOrName string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] return c.delete(clustersPath+"/"+id, &resp) } // GetKubeconfig retrieves the cluster kubeconfig, optionally for a specific role. -func (c *Client) GetKubeconfig(id string, role string) (string, error) { +func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return "", err + } path := clustersPath + "/" + id + "/kubeconfig" if role != "" { path += "?role=" + role @@ -133,20 +154,41 @@ func (c *Client) GetKubeconfig(id string, role string) (string, error) { } // RepairCluster triggers a re-run of critical provisioning steps. -func (c *Client) RepairCluster(id string) error { +func (c *Client) RepairCluster(idOrName string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] return c.post(clustersPath+"/"+id+"/repair", nil, &resp) } // ScaleCluster adjusts the number of worker nodes. -func (c *Client) ScaleCluster(id string, workers int) error { +func (c *Client) ScaleCluster(idOrName string, workers int) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] input := &ScaleClusterInput{Workers: workers} return c.post(clustersPath+"/"+id+"/scale", input, &resp) } // GetClusterHealth retrieved the operational health of the cluster. -func (c *Client) GetClusterHealth(id string) (*ClusterHealth, error) { +func (c *Client) GetClusterHealth(idOrName string) (*ClusterHealth, error) { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return nil, err + } var resp Response[*ClusterHealth] if err := c.get(clustersPath+"/"+id+"/health", &resp); err != nil { return nil, err @@ -160,20 +202,41 @@ type UpgradeClusterInput struct { } // UpgradeCluster initiates an asynchronous version upgrade. -func (c *Client) UpgradeCluster(id string, version string) error { +func (c *Client) UpgradeCluster(idOrName string, version string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] input := &UpgradeClusterInput{Version: version} return c.post(clustersPath+"/"+id+"/upgrade", input, &resp) } // RotateSecrets triggers a renewal of cluster certificates. -func (c *Client) RotateSecrets(id string) error { +func (c *Client) RotateSecrets(idOrName string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] return c.post(clustersPath+"/"+id+"/rotate-secrets", nil, &resp) } // CreateBackup initiates a cluster state snapshot. -func (c *Client) CreateBackup(id string) error { +func (c *Client) CreateBackup(idOrName string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] return c.post(clustersPath+"/"+id+"/backups", nil, &resp) } @@ -184,32 +247,60 @@ type RestoreBackupInput struct { } // RestoreBackup initiates a cluster restoration from a specific path. -func (c *Client) RestoreBackup(id string, backupPath string) error { +func (c *Client) RestoreBackup(idOrName string, backupPath string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + if err != nil { + return err + } var resp Response[any] input := &RestoreBackupInput{BackupPath: backupPath} return c.post(clustersPath+"/"+id+"/restore", input, &resp) } // AddNodeGroup adds a new node pool to the cluster. -func (c *Client) AddNodeGroup(clusterID string, input NodeGroupInput) (*NodeGroup, error) { +func (c *Client) AddNodeGroup(clusterIDOrName string, input NodeGroupInput) (*NodeGroup, error) { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + if err != nil { + return nil, err + } var resp Response[*NodeGroup] - if err := c.post(clustersPath+"/"+clusterID+"/nodegroups", input, &resp); err != nil { + if err := c.post(clustersPath+"/"+id+"/nodegroups", input, &resp); err != nil { return nil, err } return resp.Data, nil } // UpdateNodeGroupWithContext updates a node group's parameters with context support. -func (c *Client) UpdateNodeGroupWithContext(ctx context.Context, clusterID string, name string, input UpdateNodeGroupInput) (*NodeGroup, error) { +func (c *Client) UpdateNodeGroupWithContext(ctx context.Context, clusterIDOrName string, name string, input UpdateNodeGroupInput) (*NodeGroup, error) { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + if err != nil { + return nil, err + } var resp Response[*NodeGroup] - if err := c.putWithContext(ctx, clustersPath+"/"+clusterID+"/nodegroups/"+url.PathEscape(name), input, &resp); err != nil { + if err := c.putWithContext(ctx, clustersPath+"/"+id+"/nodegroups/"+url.PathEscape(name), input, &resp); err != nil { return nil, err } return resp.Data, nil } // DeleteNodeGroup removes a node group. -func (c *Client) DeleteNodeGroup(clusterID string, name string) error { +func (c *Client) DeleteNodeGroup(clusterIDOrName string, name string) error { + id, err := c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + if err != nil { + return err + } var resp Response[any] - return c.delete(clustersPath+"/"+clusterID+"/nodegroups/"+url.PathEscape(name), &resp) + return c.delete(clustersPath+"/"+id+"/nodegroups/"+url.PathEscape(name), &resp) } diff --git a/pkg/sdk/snapshot.go b/pkg/sdk/snapshot.go index 9ccb7b09b..544ba3f32 100644 --- a/pkg/sdk/snapshot.go +++ b/pkg/sdk/snapshot.go @@ -4,18 +4,24 @@ package sdk import ( "fmt" - "github.com/google/uuid" "github.com/poyrazk/thecloud/internal/core/domain" ) -func (c *Client) CreateSnapshot(volumeID uuid.UUID, description string) (*domain.Snapshot, error) { +func (c *Client) CreateSnapshot(volumeIDOrName string, description string) (*domain.Snapshot, error) { + id, err := c.resolveID("volume", func() ([]interface{}, error) { + vols, err := c.ListVolumes() + return interfaceSlice(vols), err + }, func(v interface{}) string { return v.(Volume).ID.String() }, func(v interface{}) string { return v.(Volume).Name }, volumeIDOrName) + if err != nil { + return nil, err + } req := map[string]interface{}{ - "volume_id": volumeID, + "volume_id": id, "description": description, } var snapshot domain.Snapshot - err := c.post("/snapshots", req, &snapshot) + err = c.post("/snapshots", req, &snapshot) if err != nil { return nil, err } diff --git a/pkg/sdk/snapshot_test.go b/pkg/sdk/snapshot_test.go index 71361063a..89a167a31 100644 --- a/pkg/sdk/snapshot_test.go +++ b/pkg/sdk/snapshot_test.go @@ -25,6 +25,7 @@ const ( func TestClientCreateSnapshot(t *testing.T) { volumeID := uuid.New() + volumeIDStr := volumeID.String() expectedSnapshot := domain.Snapshot{ ID: uuid.New(), VolumeID: volumeID, @@ -34,13 +35,20 @@ func TestClientCreateSnapshot(t *testing.T) { } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/volumes" && r.Method == http.MethodGet { + w.Header().Set(snapshotContentType, snapshotApplicationJSON) + _ = json.NewEncoder(w).Encode(Response[[]Volume]{ + Data: []Volume{{ID: volumeID, Name: "test-volume"}}, + }) + return + } assert.Equal(t, snapshotPath, r.URL.Path) assert.Equal(t, http.MethodPost, r.Method) var req map[string]interface{} err := json.NewDecoder(r.Body).Decode(&req) assert.NoError(t, err) - assert.Equal(t, volumeID.String(), req["volume_id"]) + assert.Equal(t, volumeIDStr, req["volume_id"]) assert.Equal(t, expectedSnapshot.Description, req["description"]) w.Header().Set(snapshotContentType, snapshotApplicationJSON) @@ -49,7 +57,7 @@ func TestClientCreateSnapshot(t *testing.T) { defer server.Close() client := NewClient(server.URL, snapshotAPIKey) - snapshot, err := client.CreateSnapshot(volumeID, snapshotDescription) + snapshot, err := client.CreateSnapshot(volumeIDStr, snapshotDescription) require.NoError(t, err) assert.NotNil(t, snapshot) @@ -157,7 +165,7 @@ func TestClientSnapshotErrors(t *testing.T) { defer server.Close() client := NewClient(server.URL, snapshotAPIKey) - _, err := client.CreateSnapshot(uuid.New(), "snap") + _, err := client.CreateSnapshot(uuid.New().String(), "snap") require.Error(t, err) _, err = client.ListSnapshots() From 7b9c22e912984f2154b234c7f4d52eadb55964fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 21:41:18 +0300 Subject: [PATCH 2/8] fix: update CCM test mocks for list-then-get ID resolution pattern --- internal/ccm/instances_test.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/ccm/instances_test.go b/internal/ccm/instances_test.go index 43ed792bb..d57c7aa9d 100644 --- a/internal/ccm/instances_test.go +++ b/internal/ccm/instances_test.go @@ -21,7 +21,16 @@ func TestInstancesV2(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") switch r.URL.Path { - case "/instances/test-node": + case "/instances": + // List instances for resolveID lookup + instances := []sdk.Instance{ + {ID: "inst-123", Name: "test-node", PrivateIP: "10.0.0.5", Status: "RUNNING", InstanceType: "standard-2"}, + {ID: "inst-456", Name: "stopped-node", Status: StatusStopped}, + } + if err := json.NewEncoder(w).Encode(sdk.Response[[]sdk.Instance]{Data: instances}); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + case "/instances/inst-123": inst := sdk.Instance{ ID: "inst-123", Name: "test-node", @@ -32,7 +41,7 @@ func TestInstancesV2(t *testing.T) { if err := json.NewEncoder(w).Encode(sdk.Response[sdk.Instance]{Data: inst}); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) } - case "/instances/stopped-node": + case "/instances/inst-456": inst := sdk.Instance{ ID: "inst-456", Status: StatusStopped, From 160863953d6f239d79ac8ff5140fedbc88508275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 21:59:04 +0300 Subject: [PATCH 3/8] fix: update CSI test mocks for list-then-get ID resolution pattern --- internal/csi/driver_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/csi/driver_test.go b/internal/csi/driver_test.go index 56d81883e..b6e4b62c7 100644 --- a/internal/csi/driver_test.go +++ b/internal/csi/driver_test.go @@ -160,7 +160,12 @@ func TestDriver_ControllerServer(t *testing.T) { w.Write([]byte(`{"data": "success"}`)) return } - if r.Method == "GET" && r.URL.Path == "/instances/node-1" { + if r.Method == "GET" && r.URL.Path == "/instances" { + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"data": [{"id": "inst-123", "name": "node-1"}]}`)) + return + } + if r.Method == "GET" && r.URL.Path == "/instances/inst-123" { w.WriteHeader(http.StatusOK) w.Write([]byte(`{"data": {"id": "inst-123", "name": "node-1"}}`)) return From cef4754af3e6976220bee3c333ac8dd78d681ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 22:12:45 +0300 Subject: [PATCH 4/8] refactor: add context support to CreateSnapshot and resolveIDWithContext - Add ListVolumesWithContext(ctx context.Context) to volume.go - Add resolveIDWithContext to client.go for context-aware ID resolution - Update CreateSnapshot to take context.Context and use resolveIDWithContext - Update GetInstanceWithContext and TerminateInstanceWithContext to use ListInstancesWithContext - Update CLI snapshot create command to pass context.Context - Update test calls to include context.Background() --- cmd/cloud/snapshot.go | 4 ++-- pkg/sdk/client.go | 37 +++++++++++++++++++++++++++++++++++++ pkg/sdk/compute.go | 4 ++-- pkg/sdk/snapshot.go | 9 +++++---- pkg/sdk/snapshot_test.go | 11 ++++++++--- pkg/sdk/volume.go | 9 +++++++++ 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/cmd/cloud/snapshot.go b/cmd/cloud/snapshot.go index 5b174d084..856a13b98 100644 --- a/cmd/cloud/snapshot.go +++ b/cmd/cloud/snapshot.go @@ -59,7 +59,7 @@ var snapshotListCmd = &cobra.Command{ } var snapshotCreateCmd = &cobra.Command{ - Use: "create [volume-id]", + Use: "create [volume-id|volume-name]", Short: "Create a snapshot from a volume", Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { @@ -67,7 +67,7 @@ var snapshotCreateCmd = &cobra.Command{ desc, _ := cmd.Flags().GetString("desc") client := createClient(opts) - snapshot, err := client.CreateSnapshot(volIDOrName, desc) + snapshot, err := client.CreateSnapshot(cmd.Context(), volIDOrName, desc) if err != nil { fmt.Printf(snapshotErrorFormat, err) return diff --git a/pkg/sdk/client.go b/pkg/sdk/client.go index 25f1bdca8..4ca889cca 100644 --- a/pkg/sdk/client.go +++ b/pkg/sdk/client.go @@ -216,6 +216,43 @@ func (c *Client) resolveID(resourceType string, listFn func() ([]interface{}, er return matches[0], nil } +// resolveIDWithContext resolves a partial ID or name to a full UUID with context support. +func (c *Client) resolveIDWithContext(ctx context.Context, resourceType string, listFn func(context.Context) ([]interface{}, error), getID func(interface{}) string, getName func(interface{}) string, idOrName string) (string, error) { + // If it's a valid UUID, use it directly + if _, err := uuid.Parse(idOrName); err == nil { + return idOrName, nil + } + + // Try to resolve by name or prefix + items, err := listFn(ctx) + if err != nil { + return "", err + } + + // Check for exact name match + for _, item := range items { + if getName(item) == idOrName { + return getID(item), nil + } + } + + // Try prefix match - track matches for ambiguity check + var matches []string + for _, item := range items { + if strings.HasPrefix(getID(item), idOrName) { + matches = append(matches, getID(item)) + } + } + + if len(matches) == 0 { + return "", fmt.Errorf("%s not found: %s", resourceType, idOrName) + } + if len(matches) > 1 { + return "", fmt.Errorf("%s ambiguous: %s matches %d resources", resourceType, idOrName, len(matches)) + } + return matches[0], nil +} + // interfaceSlice converts a slice of any type to []interface{} func interfaceSlice[T any](slice []T) []interface{} { result := make([]interface{}, len(slice)) diff --git a/pkg/sdk/compute.go b/pkg/sdk/compute.go index c7ab2dd2b..402e4e521 100644 --- a/pkg/sdk/compute.go +++ b/pkg/sdk/compute.go @@ -64,7 +64,7 @@ func (c *Client) GetInstance(idOrName string) (*Instance, error) { // GetInstanceWithContext retrieves a compute instance with context support. func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (*Instance, error) { id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() + instances, err := c.ListInstancesWithContext(ctx) return interfaceSlice(instances), err }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) if err != nil { @@ -156,7 +156,7 @@ func (c *Client) TerminateInstance(idOrName string) error { // TerminateInstanceWithContext deletes an instance with context support. func (c *Client) TerminateInstanceWithContext(ctx context.Context, idOrName string) error { id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() + instances, err := c.ListInstancesWithContext(ctx) return interfaceSlice(instances), err }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) if err != nil { diff --git a/pkg/sdk/snapshot.go b/pkg/sdk/snapshot.go index 544ba3f32..759c6fccd 100644 --- a/pkg/sdk/snapshot.go +++ b/pkg/sdk/snapshot.go @@ -2,14 +2,15 @@ package sdk import ( + "context" "fmt" "github.com/poyrazk/thecloud/internal/core/domain" ) -func (c *Client) CreateSnapshot(volumeIDOrName string, description string) (*domain.Snapshot, error) { - id, err := c.resolveID("volume", func() ([]interface{}, error) { - vols, err := c.ListVolumes() +func (c *Client) CreateSnapshot(ctx context.Context, volumeIDOrName string, description string) (*domain.Snapshot, error) { + id, err := c.resolveIDWithContext(ctx, "volume", func(ctx context.Context) ([]interface{}, error) { + vols, err := c.ListVolumesWithContext(ctx) return interfaceSlice(vols), err }, func(v interface{}) string { return v.(Volume).ID.String() }, func(v interface{}) string { return v.(Volume).Name }, volumeIDOrName) if err != nil { @@ -21,7 +22,7 @@ func (c *Client) CreateSnapshot(volumeIDOrName string, description string) (*dom } var snapshot domain.Snapshot - err = c.post("/snapshots", req, &snapshot) + err = c.postWithContext(ctx, "/snapshots", req, &snapshot) if err != nil { return nil, err } diff --git a/pkg/sdk/snapshot_test.go b/pkg/sdk/snapshot_test.go index 89a167a31..2543261bc 100644 --- a/pkg/sdk/snapshot_test.go +++ b/pkg/sdk/snapshot_test.go @@ -1,6 +1,7 @@ package sdk import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -37,9 +38,13 @@ func TestClientCreateSnapshot(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/volumes" && r.Method == http.MethodGet { w.Header().Set(snapshotContentType, snapshotApplicationJSON) - _ = json.NewEncoder(w).Encode(Response[[]Volume]{ + err := json.NewEncoder(w).Encode(Response[[]Volume]{ Data: []Volume{{ID: volumeID, Name: "test-volume"}}, }) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } return } assert.Equal(t, snapshotPath, r.URL.Path) @@ -57,7 +62,7 @@ func TestClientCreateSnapshot(t *testing.T) { defer server.Close() client := NewClient(server.URL, snapshotAPIKey) - snapshot, err := client.CreateSnapshot(volumeIDStr, snapshotDescription) + snapshot, err := client.CreateSnapshot(context.Background(), volumeIDStr, snapshotDescription) require.NoError(t, err) assert.NotNil(t, snapshot) @@ -165,7 +170,7 @@ func TestClientSnapshotErrors(t *testing.T) { defer server.Close() client := NewClient(server.URL, snapshotAPIKey) - _, err := client.CreateSnapshot(uuid.New().String(), "snap") + _, err := client.CreateSnapshot(context.Background(), uuid.New().String(), "snap") require.Error(t, err) _, err = client.ListSnapshots() diff --git a/pkg/sdk/volume.go b/pkg/sdk/volume.go index 07c68e8bb..e4ed28539 100644 --- a/pkg/sdk/volume.go +++ b/pkg/sdk/volume.go @@ -2,6 +2,7 @@ package sdk import ( + "context" "fmt" "time" @@ -28,6 +29,14 @@ func (c *Client) ListVolumes() ([]Volume, error) { return res.Data, nil } +func (c *Client) ListVolumesWithContext(ctx context.Context) ([]Volume, error) { + var res Response[[]Volume] + if err := c.getWithContext(ctx, "/volumes", &res); err != nil { + return nil, err + } + return res.Data, nil +} + func (c *Client) CreateVolume(name string, sizeGB int) (*Volume, error) { body := map[string]interface{}{ "name": name, From 07cc8707833e8adf7822fe5a0d1fe1d109c5fef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 22:33:59 +0300 Subject: [PATCH 5/8] refactor: extract resolveID helpers and fix context propagation - Add resolveInstanceID/resolveInstanceIDWithContext helpers in compute.go - Add resolveClusterID/resolveClusterIDWithContext helpers in kubernetes.go - Add ListClustersWithContext for proper context propagation - Update GetClusterWithContext and UpdateNodeGroupWithContext to use context-aware resolution - Refactor 9 call sites in compute.go and 13 in kubernetes.go to use helpers --- pkg/sdk/compute.go | 59 ++++++++++++---------------- pkg/sdk/kubernetes.go | 90 ++++++++++++++++++------------------------- 2 files changed, 62 insertions(+), 87 deletions(-) diff --git a/pkg/sdk/compute.go b/pkg/sdk/compute.go index 402e4e521..a2d53640c 100644 --- a/pkg/sdk/compute.go +++ b/pkg/sdk/compute.go @@ -45,12 +45,25 @@ func (c *Client) ListInstancesWithContext(ctx context.Context) ([]Instance, erro return res.Data, nil } -// GetInstance retrieves a compute instance by ID or name. -func (c *Client) GetInstance(idOrName string) (*Instance, error) { - id, err := c.resolveID("instance", func() ([]interface{}, error) { +// resolveInstanceID resolves an instance ID or name to a full ID. +func (c *Client) resolveInstanceID(idOrName string) (string, error) { + return c.resolveID("instance", func() ([]interface{}, error) { instances, err := c.ListInstances() return interfaceSlice(instances), err }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) +} + +// resolveInstanceIDWithContext resolves an instance ID or name to a full ID with context support. +func (c *Client) resolveInstanceIDWithContext(ctx context.Context, idOrName string) (string, error) { + return c.resolveID("instance", func() ([]interface{}, error) { + instances, err := c.ListInstancesWithContext(ctx) + return interfaceSlice(instances), err + }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) +} + +// GetInstance retrieves a compute instance by ID or name. +func (c *Client) GetInstance(idOrName string) (*Instance, error) { + id, err := c.resolveInstanceID(idOrName) if err != nil { return nil, err } @@ -63,10 +76,7 @@ func (c *Client) GetInstance(idOrName string) (*Instance, error) { // GetInstanceWithContext retrieves a compute instance with context support. func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (*Instance, error) { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstancesWithContext(ctx) - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return nil, err } @@ -78,10 +88,7 @@ func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (* } func (c *Client) GetConsoleURL(idOrName string) (string, error) { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return "", err } @@ -122,10 +129,7 @@ func (c *Client) LaunchInstance(name, image, ports, instanceType string, vpcID, // UpdateInstanceMetadata updates the metadata and labels of an instance. func (c *Client) UpdateInstanceMetadata(idOrName string, metadata, labels map[string]string) error { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return err } @@ -138,10 +142,7 @@ func (c *Client) UpdateInstanceMetadata(idOrName string, metadata, labels map[st // StopInstance stops a running instance by ID or name. func (c *Client) StopInstance(idOrName string) error { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return err } @@ -155,10 +156,7 @@ func (c *Client) TerminateInstance(idOrName string) error { // TerminateInstanceWithContext deletes an instance with context support. func (c *Client) TerminateInstanceWithContext(ctx context.Context, idOrName string) error { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstancesWithContext(ctx) - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return err } @@ -167,10 +165,7 @@ func (c *Client) TerminateInstanceWithContext(ctx context.Context, idOrName stri // GetInstanceLogs retrieves the raw log output for an instance. func (c *Client) GetInstanceLogs(idOrName string) (string, error) { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return "", err } @@ -186,10 +181,7 @@ func (c *Client) GetInstanceLogs(idOrName string) (string, error) { // ResizeInstance changes the instance type of a running or stopped instance. func (c *Client) ResizeInstance(idOrName, newInstanceType string) error { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return err } @@ -209,10 +201,7 @@ type InstanceStats struct { // GetInstanceStats returns resource usage metrics for an instance. func (c *Client) GetInstanceStats(idOrName string) (*InstanceStats, error) { - id, err := c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) + id, err := c.resolveInstanceID(idOrName) if err != nil { return nil, err } diff --git a/pkg/sdk/kubernetes.go b/pkg/sdk/kubernetes.go index f61b0aa21..69b817163 100644 --- a/pkg/sdk/kubernetes.go +++ b/pkg/sdk/kubernetes.go @@ -90,6 +90,31 @@ func (c *Client) ListClusters() ([]*Cluster, error) { return resp.Data, nil } +// ListClustersWithContext returns all clusters for the current user with context support. +func (c *Client) ListClustersWithContext(ctx context.Context) ([]*Cluster, error) { + var resp Response[[]*Cluster] + if err := c.getWithContext(ctx, clustersPath, &resp); err != nil { + return nil, err + } + return resp.Data, nil +} + +// resolveClusterID resolves a cluster ID or name to a full UUID. +func (c *Client) resolveClusterID(idOrName string) (string, error) { + return c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClusters() + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) +} + +// resolveClusterIDWithContext resolves a cluster ID or name to a full UUID with context support. +func (c *Client) resolveClusterIDWithContext(ctx context.Context, idOrName string) (string, error) { + return c.resolveID("cluster", func() ([]interface{}, error) { + clusters, err := c.ListClustersWithContext(ctx) + return interfaceSlicePtr(clusters), err + }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) +} + // CreateCluster initiates cluster provisioning. func (c *Client) CreateCluster(input *CreateClusterInput) (*Cluster, error) { var resp Response[*Cluster] @@ -106,10 +131,7 @@ func (c *Client) GetCluster(idOrName string) (*Cluster, error) { // GetClusterWithContext retrieves cluster details by ID or name with context support. func (c *Client) GetClusterWithContext(ctx context.Context, idOrName string) (*Cluster, error) { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return nil, err } @@ -122,10 +144,7 @@ func (c *Client) GetClusterWithContext(ctx context.Context, idOrName string) (*C // DeleteCluster removes a cluster. func (c *Client) DeleteCluster(idOrName string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -135,10 +154,7 @@ func (c *Client) DeleteCluster(idOrName string) error { // GetKubeconfig retrieves the cluster kubeconfig, optionally for a specific role. func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return "", err } @@ -155,10 +171,7 @@ func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { // RepairCluster triggers a re-run of critical provisioning steps. func (c *Client) RepairCluster(idOrName string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -168,10 +181,7 @@ func (c *Client) RepairCluster(idOrName string) error { // ScaleCluster adjusts the number of worker nodes. func (c *Client) ScaleCluster(idOrName string, workers int) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -182,10 +192,7 @@ func (c *Client) ScaleCluster(idOrName string, workers int) error { // GetClusterHealth retrieved the operational health of the cluster. func (c *Client) GetClusterHealth(idOrName string) (*ClusterHealth, error) { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return nil, err } @@ -203,10 +210,7 @@ type UpgradeClusterInput struct { // UpgradeCluster initiates an asynchronous version upgrade. func (c *Client) UpgradeCluster(idOrName string, version string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -217,10 +221,7 @@ func (c *Client) UpgradeCluster(idOrName string, version string) error { // RotateSecrets triggers a renewal of cluster certificates. func (c *Client) RotateSecrets(idOrName string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -230,10 +231,7 @@ func (c *Client) RotateSecrets(idOrName string) error { // CreateBackup initiates a cluster state snapshot. func (c *Client) CreateBackup(idOrName string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -248,10 +246,7 @@ type RestoreBackupInput struct { // RestoreBackup initiates a cluster restoration from a specific path. func (c *Client) RestoreBackup(idOrName string, backupPath string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) + id, err := c.resolveClusterID(idOrName) if err != nil { return err } @@ -262,10 +257,7 @@ func (c *Client) RestoreBackup(idOrName string, backupPath string) error { // AddNodeGroup adds a new node pool to the cluster. func (c *Client) AddNodeGroup(clusterIDOrName string, input NodeGroupInput) (*NodeGroup, error) { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + id, err := c.resolveClusterID(clusterIDOrName) if err != nil { return nil, err } @@ -278,10 +270,7 @@ func (c *Client) AddNodeGroup(clusterIDOrName string, input NodeGroupInput) (*No // UpdateNodeGroupWithContext updates a node group's parameters with context support. func (c *Client) UpdateNodeGroupWithContext(ctx context.Context, clusterIDOrName string, name string, input UpdateNodeGroupInput) (*NodeGroup, error) { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + id, err := c.resolveClusterIDWithContext(ctx, clusterIDOrName) if err != nil { return nil, err } @@ -294,10 +283,7 @@ func (c *Client) UpdateNodeGroupWithContext(ctx context.Context, clusterIDOrName // DeleteNodeGroup removes a node group. func (c *Client) DeleteNodeGroup(clusterIDOrName string, name string) error { - id, err := c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, clusterIDOrName) + id, err := c.resolveClusterID(clusterIDOrName) if err != nil { return err } From a626e9c1ee0c4b7b38fbbfc230ff9d10bcfda072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 22:42:45 +0300 Subject: [PATCH 6/8] feat(sdk): add WithContext variants to kubernetes and compute modules kubernetes.go - 11 new WithContext methods: - DeleteClusterWithContext, GetKubeconfigWithContext, RepairClusterWithContext - ScaleClusterWithContext, GetClusterHealthWithContext, UpgradeClusterWithContext - RotateSecretsWithContext, CreateBackupWithContext, RestoreBackupWithContext - AddNodeGroupWithContext, DeleteNodeGroupWithContext compute.go - 6 new WithContext methods: - GetConsoleURLWithContext, UpdateInstanceMetadataWithContext, StopInstanceWithContext - GetInstanceLogsWithContext, ResizeInstanceWithContext, GetInstanceStatsWithContext - Refactored GetInstance to delegate to GetInstanceWithContext All WithContext methods use resolveIDWithContext for proper context propagation. --- pkg/sdk/compute.go | 58 ++++++++++++++++--------- pkg/sdk/kubernetes.go | 99 +++++++++++++++++++++++++++++++++---------- 2 files changed, 114 insertions(+), 43 deletions(-) diff --git a/pkg/sdk/compute.go b/pkg/sdk/compute.go index a2d53640c..9b4f9ee7f 100644 --- a/pkg/sdk/compute.go +++ b/pkg/sdk/compute.go @@ -63,15 +63,7 @@ func (c *Client) resolveInstanceIDWithContext(ctx context.Context, idOrName stri // GetInstance retrieves a compute instance by ID or name. func (c *Client) GetInstance(idOrName string) (*Instance, error) { - id, err := c.resolveInstanceID(idOrName) - if err != nil { - return nil, err - } - var res Response[Instance] - if err := c.get(fmt.Sprintf("/instances/%s", id), &res); err != nil { - return nil, err - } - return &res.Data, nil + return c.GetInstanceWithContext(context.Background(), idOrName) } // GetInstanceWithContext retrieves a compute instance with context support. @@ -88,12 +80,16 @@ func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (* } func (c *Client) GetConsoleURL(idOrName string) (string, error) { - id, err := c.resolveInstanceID(idOrName) + return c.GetConsoleURLWithContext(context.Background(), idOrName) +} + +func (c *Client) GetConsoleURLWithContext(ctx context.Context, idOrName string) (string, error) { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return "", err } var res Response[string] - if err := c.get(fmt.Sprintf("/instances/%s/console", id), &res); err != nil { + if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s/console", id), &res); err != nil { return "", err } return res.Data, nil @@ -129,7 +125,11 @@ func (c *Client) LaunchInstance(name, image, ports, instanceType string, vpcID, // UpdateInstanceMetadata updates the metadata and labels of an instance. func (c *Client) UpdateInstanceMetadata(idOrName string, metadata, labels map[string]string) error { - id, err := c.resolveInstanceID(idOrName) + return c.UpdateInstanceMetadataWithContext(context.Background(), idOrName, metadata, labels) +} + +func (c *Client) UpdateInstanceMetadataWithContext(ctx context.Context, idOrName string, metadata, labels map[string]string) error { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return err } @@ -137,16 +137,20 @@ func (c *Client) UpdateInstanceMetadata(idOrName string, metadata, labels map[st "metadata": metadata, "labels": labels, } - return c.put(fmt.Sprintf("/instances/%s/metadata", id), body, nil) + return c.putWithContext(ctx, fmt.Sprintf("/instances/%s/metadata", id), body, nil) } // StopInstance stops a running instance by ID or name. func (c *Client) StopInstance(idOrName string) error { - id, err := c.resolveInstanceID(idOrName) + return c.StopInstanceWithContext(context.Background(), idOrName) +} + +func (c *Client) StopInstanceWithContext(ctx context.Context, idOrName string) error { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return err } - return c.post(fmt.Sprintf("/instances/%s/stop", id), nil, nil) + return c.postWithContext(ctx, fmt.Sprintf("/instances/%s/stop", id), nil, nil) } // TerminateInstance deletes an instance by ID or name. @@ -165,11 +169,15 @@ func (c *Client) TerminateInstanceWithContext(ctx context.Context, idOrName stri // GetInstanceLogs retrieves the raw log output for an instance. func (c *Client) GetInstanceLogs(idOrName string) (string, error) { - id, err := c.resolveInstanceID(idOrName) + return c.GetInstanceLogsWithContext(context.Background(), idOrName) +} + +func (c *Client) GetInstanceLogsWithContext(ctx context.Context, idOrName string) (string, error) { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return "", err } - resp, err := c.resty.R().Get(c.apiURL + fmt.Sprintf("/instances/%s/logs", id)) + resp, err := c.resty.R().SetContext(ctx).Get(c.apiURL + fmt.Sprintf("/instances/%s/logs", id)) if err != nil { return "", err } @@ -181,14 +189,18 @@ func (c *Client) GetInstanceLogs(idOrName string) (string, error) { // ResizeInstance changes the instance type of a running or stopped instance. func (c *Client) ResizeInstance(idOrName, newInstanceType string) error { - id, err := c.resolveInstanceID(idOrName) + return c.ResizeInstanceWithContext(context.Background(), idOrName, newInstanceType) +} + +func (c *Client) ResizeInstanceWithContext(ctx context.Context, idOrName, newInstanceType string) error { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return err } body := map[string]string{ "instance_type": newInstanceType, } - return c.post(fmt.Sprintf("/instances/%s/resize", id), body, nil) + return c.postWithContext(ctx, fmt.Sprintf("/instances/%s/resize", id), body, nil) } // InstanceStats captures resource usage for an instance. @@ -201,12 +213,16 @@ type InstanceStats struct { // GetInstanceStats returns resource usage metrics for an instance. func (c *Client) GetInstanceStats(idOrName string) (*InstanceStats, error) { - id, err := c.resolveInstanceID(idOrName) + return c.GetInstanceStatsWithContext(context.Background(), idOrName) +} + +func (c *Client) GetInstanceStatsWithContext(ctx context.Context, idOrName string) (*InstanceStats, error) { + id, err := c.resolveInstanceIDWithContext(ctx, idOrName) if err != nil { return nil, err } var res Response[InstanceStats] - if err := c.get(fmt.Sprintf("/instances/%s/stats", id), &res); err != nil { + if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s/stats", id), &res); err != nil { return nil, err } return &res.Data, nil diff --git a/pkg/sdk/kubernetes.go b/pkg/sdk/kubernetes.go index 69b817163..4085f8a33 100644 --- a/pkg/sdk/kubernetes.go +++ b/pkg/sdk/kubernetes.go @@ -144,17 +144,27 @@ func (c *Client) GetClusterWithContext(ctx context.Context, idOrName string) (*C // DeleteCluster removes a cluster. func (c *Client) DeleteCluster(idOrName string) error { - id, err := c.resolveClusterID(idOrName) + return c.DeleteClusterWithContext(context.Background(), idOrName) +} + +// DeleteClusterWithContext removes a cluster with context support. +func (c *Client) DeleteClusterWithContext(ctx context.Context, idOrName string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] - return c.delete(clustersPath+"/"+id, &resp) + return c.deleteWithContext(ctx, clustersPath+"/"+id, &resp) } // GetKubeconfig retrieves the cluster kubeconfig, optionally for a specific role. func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { - id, err := c.resolveClusterID(idOrName) + return c.GetKubeconfigWithContext(context.Background(), idOrName, role) +} + +// GetKubeconfigWithContext retrieves the cluster kubeconfig with context support. +func (c *Client) GetKubeconfigWithContext(ctx context.Context, idOrName string, role string) (string, error) { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return "", err } @@ -163,7 +173,7 @@ func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { path += "?role=" + role } var resp Response[string] - if err := c.get(path, &resp); err != nil { + if err := c.getWithContext(ctx, path, &resp); err != nil { return "", err } return resp.Data, nil @@ -171,33 +181,48 @@ func (c *Client) GetKubeconfig(idOrName string, role string) (string, error) { // RepairCluster triggers a re-run of critical provisioning steps. func (c *Client) RepairCluster(idOrName string) error { - id, err := c.resolveClusterID(idOrName) + return c.RepairClusterWithContext(context.Background(), idOrName) +} + +// RepairClusterWithContext triggers a re-run of critical provisioning steps with context support. +func (c *Client) RepairClusterWithContext(ctx context.Context, idOrName string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] - return c.post(clustersPath+"/"+id+"/repair", nil, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/repair", nil, &resp) } // ScaleCluster adjusts the number of worker nodes. func (c *Client) ScaleCluster(idOrName string, workers int) error { - id, err := c.resolveClusterID(idOrName) + return c.ScaleClusterWithContext(context.Background(), idOrName, workers) +} + +// ScaleClusterWithContext adjusts the number of worker nodes with context support. +func (c *Client) ScaleClusterWithContext(ctx context.Context, idOrName string, workers int) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] input := &ScaleClusterInput{Workers: workers} - return c.post(clustersPath+"/"+id+"/scale", input, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/scale", input, &resp) } // GetClusterHealth retrieved the operational health of the cluster. func (c *Client) GetClusterHealth(idOrName string) (*ClusterHealth, error) { - id, err := c.resolveClusterID(idOrName) + return c.GetClusterHealthWithContext(context.Background(), idOrName) +} + +// GetClusterHealthWithContext retrieves the operational health of the cluster with context support. +func (c *Client) GetClusterHealthWithContext(ctx context.Context, idOrName string) (*ClusterHealth, error) { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return nil, err } var resp Response[*ClusterHealth] - if err := c.get(clustersPath+"/"+id+"/health", &resp); err != nil { + if err := c.getWithContext(ctx, clustersPath+"/"+id+"/health", &resp); err != nil { return nil, err } return resp.Data, nil @@ -210,33 +235,48 @@ type UpgradeClusterInput struct { // UpgradeCluster initiates an asynchronous version upgrade. func (c *Client) UpgradeCluster(idOrName string, version string) error { - id, err := c.resolveClusterID(idOrName) + return c.UpgradeClusterWithContext(context.Background(), idOrName, version) +} + +// UpgradeClusterWithContext initiates an asynchronous version upgrade with context support. +func (c *Client) UpgradeClusterWithContext(ctx context.Context, idOrName string, version string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] input := &UpgradeClusterInput{Version: version} - return c.post(clustersPath+"/"+id+"/upgrade", input, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/upgrade", input, &resp) } // RotateSecrets triggers a renewal of cluster certificates. func (c *Client) RotateSecrets(idOrName string) error { - id, err := c.resolveClusterID(idOrName) + return c.RotateSecretsWithContext(context.Background(), idOrName) +} + +// RotateSecretsWithContext triggers a renewal of cluster certificates with context support. +func (c *Client) RotateSecretsWithContext(ctx context.Context, idOrName string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] - return c.post(clustersPath+"/"+id+"/rotate-secrets", nil, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/rotate-secrets", nil, &resp) } // CreateBackup initiates a cluster state snapshot. func (c *Client) CreateBackup(idOrName string) error { - id, err := c.resolveClusterID(idOrName) + return c.CreateBackupWithContext(context.Background(), idOrName) +} + +// CreateBackupWithContext initiates a cluster state snapshot with context support. +func (c *Client) CreateBackupWithContext(ctx context.Context, idOrName string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] - return c.post(clustersPath+"/"+id+"/backups", nil, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/backups", nil, &resp) } // RestoreBackupInput defines the input for restoring a cluster from backup. @@ -246,23 +286,33 @@ type RestoreBackupInput struct { // RestoreBackup initiates a cluster restoration from a specific path. func (c *Client) RestoreBackup(idOrName string, backupPath string) error { - id, err := c.resolveClusterID(idOrName) + return c.RestoreBackupWithContext(context.Background(), idOrName, backupPath) +} + +// RestoreBackupWithContext initiates a cluster restoration from a specific path with context support. +func (c *Client) RestoreBackupWithContext(ctx context.Context, idOrName string, backupPath string) error { + id, err := c.resolveClusterIDWithContext(ctx, idOrName) if err != nil { return err } var resp Response[any] input := &RestoreBackupInput{BackupPath: backupPath} - return c.post(clustersPath+"/"+id+"/restore", input, &resp) + return c.postWithContext(ctx, clustersPath+"/"+id+"/restore", input, &resp) } // AddNodeGroup adds a new node pool to the cluster. func (c *Client) AddNodeGroup(clusterIDOrName string, input NodeGroupInput) (*NodeGroup, error) { - id, err := c.resolveClusterID(clusterIDOrName) + return c.AddNodeGroupWithContext(context.Background(), clusterIDOrName, input) +} + +// AddNodeGroupWithContext adds a new node pool to the cluster with context support. +func (c *Client) AddNodeGroupWithContext(ctx context.Context, clusterIDOrName string, input NodeGroupInput) (*NodeGroup, error) { + id, err := c.resolveClusterIDWithContext(ctx, clusterIDOrName) if err != nil { return nil, err } var resp Response[*NodeGroup] - if err := c.post(clustersPath+"/"+id+"/nodegroups", input, &resp); err != nil { + if err := c.postWithContext(ctx, clustersPath+"/"+id+"/nodegroups", input, &resp); err != nil { return nil, err } return resp.Data, nil @@ -283,10 +333,15 @@ func (c *Client) UpdateNodeGroupWithContext(ctx context.Context, clusterIDOrName // DeleteNodeGroup removes a node group. func (c *Client) DeleteNodeGroup(clusterIDOrName string, name string) error { - id, err := c.resolveClusterID(clusterIDOrName) + return c.DeleteNodeGroupWithContext(context.Background(), clusterIDOrName, name) +} + +// DeleteNodeGroupWithContext removes a node group with context support. +func (c *Client) DeleteNodeGroupWithContext(ctx context.Context, clusterIDOrName string, name string) error { + id, err := c.resolveClusterIDWithContext(ctx, clusterIDOrName) if err != nil { return err } var resp Response[any] - return c.delete(clustersPath+"/"+id+"/nodegroups/"+url.PathEscape(name), &resp) + return c.deleteWithContext(ctx, clustersPath+"/"+id+"/nodegroups/"+url.PathEscape(name), &resp) } From ddc63a0e76cf9aa5313ca1daf0b8f3ef65bb57dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 22:49:03 +0300 Subject: [PATCH 7/8] fix: remove unused resolveInstanceID and resolveClusterID helpers These non-context-aware helpers are no longer used since all call sites now use the *WithContext variants for proper context support. --- pkg/sdk/compute.go | 8 -------- pkg/sdk/kubernetes.go | 8 -------- 2 files changed, 16 deletions(-) diff --git a/pkg/sdk/compute.go b/pkg/sdk/compute.go index 9b4f9ee7f..a8acb5701 100644 --- a/pkg/sdk/compute.go +++ b/pkg/sdk/compute.go @@ -45,14 +45,6 @@ func (c *Client) ListInstancesWithContext(ctx context.Context) ([]Instance, erro return res.Data, nil } -// resolveInstanceID resolves an instance ID or name to a full ID. -func (c *Client) resolveInstanceID(idOrName string) (string, error) { - return c.resolveID("instance", func() ([]interface{}, error) { - instances, err := c.ListInstances() - return interfaceSlice(instances), err - }, func(v interface{}) string { return v.(Instance).ID }, func(v interface{}) string { return v.(Instance).Name }, idOrName) -} - // resolveInstanceIDWithContext resolves an instance ID or name to a full ID with context support. func (c *Client) resolveInstanceIDWithContext(ctx context.Context, idOrName string) (string, error) { return c.resolveID("instance", func() ([]interface{}, error) { diff --git a/pkg/sdk/kubernetes.go b/pkg/sdk/kubernetes.go index 4085f8a33..49fe80c98 100644 --- a/pkg/sdk/kubernetes.go +++ b/pkg/sdk/kubernetes.go @@ -99,14 +99,6 @@ func (c *Client) ListClustersWithContext(ctx context.Context) ([]*Cluster, error return resp.Data, nil } -// resolveClusterID resolves a cluster ID or name to a full UUID. -func (c *Client) resolveClusterID(idOrName string) (string, error) { - return c.resolveID("cluster", func() ([]interface{}, error) { - clusters, err := c.ListClusters() - return interfaceSlicePtr(clusters), err - }, func(v interface{}) string { return v.(*Cluster).ID.String() }, func(v interface{}) string { return v.(*Cluster).Name }, idOrName) -} - // resolveClusterIDWithContext resolves a cluster ID or name to a full UUID with context support. func (c *Client) resolveClusterIDWithContext(ctx context.Context, idOrName string) (string, error) { return c.resolveID("cluster", func() ([]interface{}, error) { From 99fdd8afb9c3a9f64b80449466d5a53571486103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=BCseyin=20Poyraz=20K=C3=BC=C3=A7=C3=BCkarslan?= Date: Thu, 7 May 2026 23:01:10 +0300 Subject: [PATCH 8/8] fix: handle GetString error and URL-encode role query param - snapshot.go: Properly handle error from cmd.Flags().GetString("desc") - kubernetes.go: URL-encode role parameter in GetKubeconfigWithContext --- cmd/cloud/snapshot.go | 6 +++++- pkg/sdk/kubernetes.go | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/cloud/snapshot.go b/cmd/cloud/snapshot.go index 856a13b98..9195c7c77 100644 --- a/cmd/cloud/snapshot.go +++ b/cmd/cloud/snapshot.go @@ -64,7 +64,11 @@ var snapshotCreateCmd = &cobra.Command{ Args: cobra.ExactArgs(1), Run: func(cmd *cobra.Command, args []string) { volIDOrName := args[0] - desc, _ := cmd.Flags().GetString("desc") + desc, err := cmd.Flags().GetString("desc") + if err != nil { + fmt.Printf(snapshotErrorFormat, err) + return + } client := createClient(opts) snapshot, err := client.CreateSnapshot(cmd.Context(), volIDOrName, desc) diff --git a/pkg/sdk/kubernetes.go b/pkg/sdk/kubernetes.go index 49fe80c98..5a72e6254 100644 --- a/pkg/sdk/kubernetes.go +++ b/pkg/sdk/kubernetes.go @@ -162,7 +162,7 @@ func (c *Client) GetKubeconfigWithContext(ctx context.Context, idOrName string, } path := clustersPath + "/" + id + "/kubeconfig" if role != "" { - path += "?role=" + role + path += "?role=" + url.QueryEscape(role) } var resp Response[string] if err := c.getWithContext(ctx, path, &resp); err != nil {