diff --git a/CHANGELOG.md b/CHANGELOG.md index e98b6a467..04cf47d60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug fixes +- compute: allow instance and instance pool operations to use template IDs omitted from template lists (#888) - compute: honor the selected template's minimum disk size when creating instances and instance pools (#882) - instance create: apply delete protection in the instance's zone, fixing a wrong-zone "Not Found" error when creating protected instances outside the default zone (#879) - sks cluster update: fix oidc config drop (#881) diff --git a/cmd/compute/instance/instance_create.go b/cmd/compute/instance/instance_create.go index d3422a5a8..6eed12eb8 100644 --- a/cmd/compute/instance/instance_create.go +++ b/cmd/compute/instance/instance_create.go @@ -89,18 +89,9 @@ func (c *instanceCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { //nol return err } - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) + template, err := compute.ResolveTemplate(ctx, client, c.Template, c.TemplateVisibility, c.Zone) if err != nil { - return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) - } - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) + return err } diskSize, err := compute.ResolveTemplateDiskSize( c.DiskSize, diff --git a/cmd/compute/instance/instance_reset.go b/cmd/compute/instance/instance_reset.go index 5543de80f..e51885622 100644 --- a/cmd/compute/instance/instance_reset.go +++ b/cmd/compute/instance/instance_reset.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/cmd/compute" "github.com/exoscale/cli/pkg/globalstate" "github.com/exoscale/cli/pkg/output" "github.com/exoscale/cli/utils" @@ -77,24 +78,17 @@ func (c *instanceResetCmd) CmdRun(_ *cobra.Command, _ []string) error { } if c.Template != "" { - - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) - + templateID, err := compute.ResolveTemplateID( + ctx, + client, + c.Template, + c.TemplateVisibility, + v3.ZoneName(c.Zone), + ) if err != nil { return err } - - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) - } - - request.Template = &template + request.Template = &v3.Template{ID: templateID} } op, err := client.ResetInstance(ctx, instance.ID, request) diff --git a/cmd/compute/instance_pool/instance_pool_create.go b/cmd/compute/instance_pool/instance_pool_create.go index 94e609ebf..7e3e1772d 100644 --- a/cmd/compute/instance_pool/instance_pool_create.go +++ b/cmd/compute/instance_pool/instance_pool_create.go @@ -71,18 +71,9 @@ func (c *instancePoolCreateCmd) CmdRun(cmd *cobra.Command, _ []string) error { return err } - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) + template, err := compute.ResolveTemplate(ctx, client, c.Template, c.TemplateVisibility, c.Zone) if err != nil { - return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) - } - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) + return err } diskSize, err := compute.ResolveTemplateDiskSize( c.DiskSize, diff --git a/cmd/compute/instance_pool/instance_pool_update.go b/cmd/compute/instance_pool/instance_pool_update.go index 73a311c08..1c4d08420 100644 --- a/cmd/compute/instance_pool/instance_pool_update.go +++ b/cmd/compute/instance_pool/instance_pool_update.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/cobra" exocmd "github.com/exoscale/cli/cmd" + "github.com/exoscale/cli/cmd/compute" "github.com/exoscale/cli/pkg/globalstate" "github.com/exoscale/cli/pkg/output" "github.com/exoscale/cli/pkg/userdata" @@ -259,20 +260,11 @@ func (c *instancePoolUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error { / } if cmd.Flags().Changed(exocmd.MustCLICommandFlagName(c, &c.Template)) { - templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(c.TemplateVisibility))) + templateID, err := compute.ResolveTemplateID(ctx, client, c.Template, c.TemplateVisibility, c.Zone) if err != nil { - return fmt.Errorf("error listing template with visibility %q: %w", c.TemplateVisibility, err) - } - template, err := templates.FindTemplate(c.Template) - if err != nil { - return fmt.Errorf( - "no template %q found with visibility %s in zone %s", - c.Template, - c.TemplateVisibility, - c.Zone, - ) + return err } - updateReq.Template = &template + updateReq.Template = &v3.Template{ID: templateID} updated = true } diff --git a/cmd/compute/template.go b/cmd/compute/template.go new file mode 100644 index 000000000..f5d7c917c --- /dev/null +++ b/cmd/compute/template.go @@ -0,0 +1,62 @@ +package compute + +import ( + "context" + "fmt" + + v3 "github.com/exoscale/egoscale/v3" +) + +// ResolveTemplate resolves names and retrieves UUID metadata when available. +func ResolveTemplate( + ctx context.Context, + client *v3.Client, + nameOrID string, + visibility string, + zone v3.ZoneName, +) (*v3.Template, error) { + if id, err := v3.ParseUUID(nameOrID); err == nil { + template, err := client.GetTemplate(ctx, id) + if err != nil { + // Metadata is optional for UUIDs; the mutation API remains authoritative. + return &v3.Template{ID: id}, nil + } + return template, nil + } + + templates, err := client.ListTemplates(ctx, v3.ListTemplatesWithVisibility(v3.ListTemplatesVisibility(visibility))) + if err != nil { + return nil, fmt.Errorf("error listing template with visibility %q: %w", visibility, err) + } + template, err := templates.FindTemplate(nameOrID) + if err != nil { + return nil, fmt.Errorf( + "no template %q found with visibility %s in zone %s", + nameOrID, + visibility, + zone, + ) + } + + return &template, nil +} + +// ResolveTemplateID passes UUIDs through and resolves template names from the list endpoint. +func ResolveTemplateID( + ctx context.Context, + client *v3.Client, + nameOrID string, + visibility string, + zone v3.ZoneName, +) (v3.UUID, error) { + if id, err := v3.ParseUUID(nameOrID); err == nil { + return id, nil + } + + template, err := ResolveTemplate(ctx, client, nameOrID, visibility, zone) + if err != nil { + return "", err + } + + return template.ID, nil +} diff --git a/cmd/compute/template_test.go b/cmd/compute/template_test.go new file mode 100644 index 000000000..3c476ccf0 --- /dev/null +++ b/cmd/compute/template_test.go @@ -0,0 +1,76 @@ +package compute + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + v3 "github.com/exoscale/egoscale/v3" + "github.com/exoscale/egoscale/v3/credentials" +) + +func TestResolveTemplateIDPassesUUIDThrough(t *testing.T) { + const id = "5b473841-fd33-4b88-bcfe-a776abf15034" + + got, err := ResolveTemplateID(context.Background(), nil, id, "public", "ch-gva-2") + if err != nil { + t.Fatalf("ResolveTemplateID() error = %v", err) + } + if got != v3.UUID(id) { + t.Errorf("ResolveTemplateID() = %q, want %q", got, id) + } +} + +func TestResolveTemplateGetsOptionalUUIDMetadata(t *testing.T) { + const id = "5b473841-fd33-4b88-bcfe-a776abf15034" + + tests := []struct { + name string + status int + response string + wantSize int64 + }{ + { + name: "metadata available", + status: http.StatusOK, + response: `{"id":"5b473841-fd33-4b88-bcfe-a776abf15034","size":85899345920}`, + wantSize: 80 << 30, + }, + { + name: "metadata unavailable", + status: http.StatusNotFound, + response: `{"message":"not found"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet || r.URL.Path != "/template/"+id { + t.Errorf("request = %s %s, want GET /template/%s", r.Method, r.URL.Path, id) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(tt.status) + _, _ = w.Write([]byte(tt.response)) + })) + defer server.Close() + + client, err := v3.NewClient( + credentials.NewStaticCredentials("key", "secret"), + v3.ClientOptWithEndpoint(v3.Endpoint(server.URL)), + ) + if err != nil { + t.Fatalf("NewClient() error = %v", err) + } + + got, err := ResolveTemplate(context.Background(), client, id, "public", "ch-gva-2") + if err != nil { + t.Fatalf("ResolveTemplate() error = %v", err) + } + if got.ID != v3.UUID(id) || got.Size != tt.wantSize { + t.Errorf("ResolveTemplate() = {ID: %q, Size: %d}, want {ID: %q, Size: %d}", got.ID, got.Size, id, tt.wantSize) + } + }) + } +}