Skip to content
20 changes: 20 additions & 0 deletions cmd/cloud/autoscaling_cli_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 6 additions & 30 deletions cmd/cloud/dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"os"
"strconv"
"strings"

"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
Expand Down Expand Up @@ -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
Expand Down
9 changes: 4 additions & 5 deletions cmd/cloud/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"
"os"

"github.com/google/uuid"
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
)
Expand Down Expand Up @@ -60,19 +59,19 @@ 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) {
volID, err := uuid.Parse(args[0])
volIDOrName := args[0]
desc, err := cmd.Flags().GetString("desc")
if err != nil {
fmt.Printf(snapshotErrorFormat, err)
return
}
desc, _ := cmd.Flags().GetString("desc")

client := createClient(opts)
snapshot, err := client.CreateSnapshot(volID, desc)
snapshot, err := client.CreateSnapshot(cmd.Context(), volIDOrName, desc)
if err != nil {
fmt.Printf(snapshotErrorFormat, err)
return
Expand Down
13 changes: 11 additions & 2 deletions internal/ccm/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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,
Expand Down
7 changes: 6 additions & 1 deletion internal/csi/driver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions pkg/sdk/autoscaling.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions pkg/sdk/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
88 changes: 74 additions & 14 deletions pkg/sdk/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,43 @@ func (c *Client) ListInstancesWithContext(ctx context.Context) ([]Instance, erro
return res.Data, nil
}

// 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) {
var res Response[Instance]
if err := c.get(fmt.Sprintf("/instances/%s", idOrName), &res); err != nil {
return nil, err
}
return &res.Data, nil
return c.GetInstanceWithContext(context.Background(), idOrName)
}

// GetInstanceWithContext retrieves a compute instance with context support.
func (c *Client) GetInstanceWithContext(ctx context.Context, idOrName string) (*Instance, error) {
id, err := c.resolveInstanceIDWithContext(ctx, 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
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func (c *Client) GetConsoleURL(idOrName string) (string, error) {
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", idOrName), &res); err != nil {
if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s/console", id), &res); err != nil {
return "", err
}
return res.Data, nil
Expand Down Expand Up @@ -100,17 +116,33 @@ 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 {
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
}
body := map[string]interface{}{
"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 {
return c.post(fmt.Sprintf("/instances/%s/stop", idOrName), nil, nil)
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.postWithContext(ctx, fmt.Sprintf("/instances/%s/stop", id), nil, nil)
}

// TerminateInstance deletes an instance by ID or name.
Expand All @@ -120,12 +152,24 @@ 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.resolveInstanceIDWithContext(ctx, 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))
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().SetContext(ctx).Get(c.apiURL + fmt.Sprintf("/instances/%s/logs", id))
if err != nil {
return "", err
}
Expand All @@ -137,10 +181,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 {
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", idOrName), body, nil)
return c.postWithContext(ctx, fmt.Sprintf("/instances/%s/resize", id), body, nil)
}

// InstanceStats captures resource usage for an instance.
Expand All @@ -153,8 +205,16 @@ type InstanceStats struct {

// GetInstanceStats returns resource usage metrics for an instance.
func (c *Client) GetInstanceStats(idOrName string) (*InstanceStats, error) {
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", idOrName), &res); err != nil {
if err := c.getWithContext(ctx, fmt.Sprintf("/instances/%s/stats", id), &res); err != nil {
return nil, err
}
return &res.Data, nil
Expand Down
Loading
Loading