diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 4738e74d7..1dc011c24 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,4 +44,4 @@ jobs: EXOSCALE_ZONE: ch-gva-2 run: | cd tests/e2e - go test -v -tags=api -timeout 30m -run TestScriptsAPI + go test -v -tags=api -timeout 30m -run TestScriptsAPI 2>&1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 491e16ef5..a86ace7b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ - `exo compute eip list` now shows the description & the management type of the EIP #803 - sks: add `rotate-karpenter-credentials` command #797 + +### Improvements + +- compute instance: enrich "not found" error with the zone searched and a hint to use -z #805 - sks: add `active-nodepool-templates` command #797 ### Bug fixes diff --git a/cmd/compute/instance/instance_console_url.go b/cmd/compute/instance/instance_console_url.go index 35f0f234b..df0a726d1 100644 --- a/cmd/compute/instance/instance_console_url.go +++ b/cmd/compute/instance/instance_console_url.go @@ -60,7 +60,7 @@ func (c *instanceConsoleURLCmd) CmdRun(cmd *cobra.Command, _ []string) error { return err } - foundInstance, err := resp.FindListInstancesResponseInstances(c.Instance) + foundInstance, err := findInstance(resp, c.Instance, string(c.Zone)) if err != nil { return err } diff --git a/cmd/compute/instance/instance_delete.go b/cmd/compute/instance/instance_delete.go index 01cbbd680..2de9c94bc 100644 --- a/cmd/compute/instance/instance_delete.go +++ b/cmd/compute/instance/instance_delete.go @@ -54,7 +54,7 @@ func (c *instanceDeleteCmd) CmdRun(_ *cobra.Command, _ []string) error { instanceToDelete := []v3.UUID{} for _, i := range c.Instances { - instance, err := instances.FindListInstancesResponseInstances(i) + instance, err := findInstance(instances, i, c.Zone) if err != nil { if !c.Force { return err diff --git a/cmd/compute/instance/instance_elastic_ip_attach.go b/cmd/compute/instance/instance_elastic_ip_attach.go index 06edf3c76..b0ffb6d0e 100644 --- a/cmd/compute/instance/instance_elastic_ip_attach.go +++ b/cmd/compute/instance/instance_elastic_ip_attach.go @@ -54,9 +54,9 @@ func (c *instanceEIPAttachCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instancesList.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instancesList, c.Instance, c.Zone) if err != nil { - return fmt.Errorf("error retrieving Instance: %w", err) + return err } elasticIPs, err := client.ListElasticIPS(ctx) diff --git a/cmd/compute/instance/instance_elastic_ip_detach.go b/cmd/compute/instance/instance_elastic_ip_detach.go index ce2493f36..234147d7a 100644 --- a/cmd/compute/instance/instance_elastic_ip_detach.go +++ b/cmd/compute/instance/instance_elastic_ip_detach.go @@ -54,7 +54,7 @@ func (c *instanceEIPDetachCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instancesList.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instancesList, c.Instance, c.Zone) if err != nil { return fmt.Errorf("error retrieving Instance: %w", err) } diff --git a/cmd/compute/instance/instance_enable_tpm.go b/cmd/compute/instance/instance_enable_tpm.go index 19cd71d2b..0f98f0a45 100644 --- a/cmd/compute/instance/instance_enable_tpm.go +++ b/cmd/compute/instance/instance_enable_tpm.go @@ -45,7 +45,7 @@ func (c *instanceEnableTPMCmd) CmdRun(_ *cobra.Command, _ []string) error { return err } - instance, err := resp.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(resp, c.Instance, string(c.Zone)) if err != nil { return err } diff --git a/cmd/compute/instance/instance_find.go b/cmd/compute/instance/instance_find.go new file mode 100644 index 000000000..39225448e --- /dev/null +++ b/cmd/compute/instance/instance_find.go @@ -0,0 +1,26 @@ +package instance + +import ( + "errors" + "fmt" + + v3 "github.com/exoscale/egoscale/v3" +) + +// findInstance looks up an instance by name or ID from a ListInstancesResponse +// and enriches the "not found" error with the zone that was searched, +// reminding the user to use -z to target a different zone. +func findInstance(resp *v3.ListInstancesResponse, nameOrID, zone string) (v3.ListInstancesResponseInstances, error) { + instance, err := resp.FindListInstancesResponseInstances(nameOrID) + if err != nil { + if errors.Is(err, v3.ErrNotFound) { + return v3.ListInstancesResponseInstances{}, fmt.Errorf( + "instance %q not found in zone %s\nHint: use -z to specify a different zone, or run 'exo compute instance list' to see instances across all zones", + nameOrID, + zone, + ) + } + return v3.ListInstancesResponseInstances{}, err + } + return instance, nil +} diff --git a/cmd/compute/instance/instance_private_network_attach.go b/cmd/compute/instance/instance_private_network_attach.go index 6bb4f95dd..d3794dce2 100644 --- a/cmd/compute/instance/instance_private_network_attach.go +++ b/cmd/compute/instance/instance_private_network_attach.go @@ -56,7 +56,7 @@ func (c *instancePrivnetAttachCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_private_network_detach.go b/cmd/compute/instance/instance_private_network_detach.go index 1961c3f4f..aa428410b 100644 --- a/cmd/compute/instance/instance_private_network_detach.go +++ b/cmd/compute/instance/instance_private_network_detach.go @@ -54,7 +54,7 @@ func (c *instancePrivnetDetachCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_private_network_updateip.go b/cmd/compute/instance/instance_private_network_updateip.go index 3a35d11b3..d6c01e03e 100644 --- a/cmd/compute/instance/instance_private_network_updateip.go +++ b/cmd/compute/instance/instance_private_network_updateip.go @@ -57,7 +57,7 @@ func (c *instancePrivnetUpdateIPCmd) CmdRun(_ *cobra.Command, _ []string) error if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_reboot.go b/cmd/compute/instance/instance_reboot.go index f3621df22..c20f053c3 100644 --- a/cmd/compute/instance/instance_reboot.go +++ b/cmd/compute/instance/instance_reboot.go @@ -44,7 +44,7 @@ func (c *instanceRebootCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_reset.go b/cmd/compute/instance/instance_reset.go index 741eb79b5..5543de80f 100644 --- a/cmd/compute/instance/instance_reset.go +++ b/cmd/compute/instance/instance_reset.go @@ -59,7 +59,7 @@ func (c *instanceResetCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_reset_password.go b/cmd/compute/instance/instance_reset_password.go index 4b651ed18..9ff1ab4d9 100644 --- a/cmd/compute/instance/instance_reset_password.go +++ b/cmd/compute/instance/instance_reset_password.go @@ -43,7 +43,7 @@ func (c *instanceResetPasswordCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_resizedisk.go b/cmd/compute/instance/instance_resizedisk.go index cc4de84d3..be3591017 100644 --- a/cmd/compute/instance/instance_resizedisk.go +++ b/cmd/compute/instance/instance_resizedisk.go @@ -52,7 +52,7 @@ func (c *instanceResizeDiskCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_reveal_password.go b/cmd/compute/instance/instance_reveal_password.go index 26f10f254..cd299b219 100644 --- a/cmd/compute/instance/instance_reveal_password.go +++ b/cmd/compute/instance/instance_reveal_password.go @@ -50,7 +50,7 @@ func (c *instanceRevealCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_scale.go b/cmd/compute/instance/instance_scale.go index ed57a37cd..910ebe274 100644 --- a/cmd/compute/instance/instance_scale.go +++ b/cmd/compute/instance/instance_scale.go @@ -55,7 +55,7 @@ func (c *instanceScaleCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_scp.go b/cmd/compute/instance/instance_scp.go index 8c2298629..85612efcb 100644 --- a/cmd/compute/instance/instance_scp.go +++ b/cmd/compute/instance/instance_scp.go @@ -105,7 +105,7 @@ func (c *instanceSCPCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_security_group_add.go b/cmd/compute/instance/instance_security_group_add.go index df858c996..022cc0161 100644 --- a/cmd/compute/instance/instance_security_group_add.go +++ b/cmd/compute/instance/instance_security_group_add.go @@ -56,7 +56,7 @@ func (c *instanceSGAddCmd) CmdRun(cmd *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_security_group_remove.go b/cmd/compute/instance/instance_security_group_remove.go index e6f0bea85..c71effed9 100644 --- a/cmd/compute/instance/instance_security_group_remove.go +++ b/cmd/compute/instance/instance_security_group_remove.go @@ -57,7 +57,7 @@ func (c *instanceSGRemoveCmd) CmdRun(cmd *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_show.go b/cmd/compute/instance/instance_show.go index c82bc20ad..b17776577 100644 --- a/cmd/compute/instance/instance_show.go +++ b/cmd/compute/instance/instance_show.go @@ -87,7 +87,7 @@ func (c *instanceShowCmd) CmdRun(cmd *cobra.Command, _ []string) error { return err } - foundInstance, err := resp.FindListInstancesResponseInstances(c.Instance) + foundInstance, err := findInstance(resp, c.Instance, string(c.Zone)) if err != nil { return err } diff --git a/cmd/compute/instance/instance_snapshot_create.go b/cmd/compute/instance/instance_snapshot_create.go index e0d027469..dfaf60958 100644 --- a/cmd/compute/instance/instance_snapshot_create.go +++ b/cmd/compute/instance/instance_snapshot_create.go @@ -52,7 +52,7 @@ func (c *instanceSnapshotCreateCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_snapshot_revert.go b/cmd/compute/instance/instance_snapshot_revert.go index 75b58a273..f2b66d469 100644 --- a/cmd/compute/instance/instance_snapshot_revert.go +++ b/cmd/compute/instance/instance_snapshot_revert.go @@ -60,7 +60,7 @@ func (c *instanceSnapshotRevertCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_ssh.go b/cmd/compute/instance/instance_ssh.go index 55470b3d4..85762b1ab 100644 --- a/cmd/compute/instance/instance_ssh.go +++ b/cmd/compute/instance/instance_ssh.go @@ -92,7 +92,7 @@ func (c *instanceSSHCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_start.go b/cmd/compute/instance/instance_start.go index 0fab93c62..30e970801 100644 --- a/cmd/compute/instance/instance_start.go +++ b/cmd/compute/instance/instance_start.go @@ -45,7 +45,7 @@ func (c *instanceStartCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_stop.go b/cmd/compute/instance/instance_stop.go index d4c8e8bd7..76f114b5d 100644 --- a/cmd/compute/instance/instance_stop.go +++ b/cmd/compute/instance/instance_stop.go @@ -44,7 +44,7 @@ func (c *instanceStopCmd) CmdRun(_ *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/cmd/compute/instance/instance_update.go b/cmd/compute/instance/instance_update.go index c02db9552..b052651d5 100644 --- a/cmd/compute/instance/instance_update.go +++ b/cmd/compute/instance/instance_update.go @@ -61,7 +61,7 @@ func (c *instanceUpdateCmd) CmdRun(cmd *cobra.Command, _ []string) error { if err != nil { return err } - instance, err := instances.FindListInstancesResponseInstances(c.Instance) + instance, err := findInstance(instances, c.Instance, c.Zone) if err != nil { return err } diff --git a/tests/e2e/scenarios/with-api/compute/instance_not_found_error.txtar b/tests/e2e/scenarios/with-api/compute/instance_not_found_error.txtar new file mode 100644 index 000000000..237dd9a86 --- /dev/null +++ b/tests/e2e/scenarios/with-api/compute/instance_not_found_error.txtar @@ -0,0 +1,20 @@ +# Test: exo compute instance returns enriched error when instance not found +# Verifies that when an instance lookup fails, the error message includes the zone +# that was searched and a hint to use -z. +# No resources are created so no teardown is required. +# TEST_ZONE is injected by the API test runner. + +# show: instance not found should mention zone and hint +! exec exo --output-format json compute instance show nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# reboot: same helper, same enriched error +! exec exo --output-format json compute instance reboot --force nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# stop: same helper, same enriched error +! exec exo --output-format json compute instance stop --force nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' diff --git a/tests/e2e/scenarios/with-api/compute/instance_not_found_error_extended.txtar b/tests/e2e/scenarios/with-api/compute/instance_not_found_error_extended.txtar new file mode 100644 index 000000000..9a0da9b9c --- /dev/null +++ b/tests/e2e/scenarios/with-api/compute/instance_not_found_error_extended.txtar @@ -0,0 +1,115 @@ +# Test: all 24 instance subcommands using findInstance produce enriched not-found errors +# Companion to instance_not_found_error.txtar which covers show/reboot/stop. +# This scenario exercises the remaining 21 commands to confirm that every +# one of them surfaces the zone and the -z hint when the instance does not exist. +# No resources are created so no teardown is required. + +# --- Actions that take only an instance name (+ optional --force) --- + +# start +! exec exo compute instance start --force nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# delete (without --force: findInstance error is returned directly; +# with --force the command prints a warning and exits 0 by design) +! exec exo compute instance delete nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# update +! exec exo compute instance update nonexistent-e2e-instance --name dummy +stderr 'not found in zone' +stderr 'Hint: use -z' + +# reset +! exec exo compute instance reset --force nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# reset-password +! exec exo compute instance reset-password nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# reveal-password +! exec exo compute instance reveal-password nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# console-url +! exec exo compute instance console-url nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# enable-tpm +! exec exo compute instance enable-tpm --force nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# snapshot create +! exec exo compute instance snapshot create nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# --- Actions that take an instance name + a second positional argument --- + +# scale (requires instance + type) +! exec exo compute instance scale --force nonexistent-e2e-instance standard.small +stderr 'not found in zone' +stderr 'Hint: use -z' + +# resize-disk (requires instance + size) +! exec exo compute instance resize-disk --force nonexistent-e2e-instance 20 +stderr 'not found in zone' +stderr 'Hint: use -z' + +# snapshot revert (requires instance + snapshot-id) +! exec exo compute instance snapshot revert --force nonexistent-e2e-instance 00000000-0000-0000-0000-000000000000 +stderr 'not found in zone' +stderr 'Hint: use -z' + +# security-group add (requires instance + sg name) +! exec exo compute instance security-group add nonexistent-e2e-instance default +stderr 'not found in zone' +stderr 'Hint: use -z' + +# security-group remove (requires instance + sg name) +! exec exo compute instance security-group remove nonexistent-e2e-instance default +stderr 'not found in zone' +stderr 'Hint: use -z' + +# private-network attach (requires instance + pn name) +! exec exo compute instance private-network attach nonexistent-e2e-instance dummy-pn +stderr 'not found in zone' +stderr 'Hint: use -z' + +# private-network detach (requires instance + pn name) +! exec exo compute instance private-network detach nonexistent-e2e-instance dummy-pn +stderr 'not found in zone' +stderr 'Hint: use -z' + +# private-network update-ip (requires instance + pn name) +! exec exo compute instance private-network update-ip nonexistent-e2e-instance dummy-pn +stderr 'not found in zone' +stderr 'Hint: use -z' + +# elastic-ip attach (requires instance + eip) +! exec exo compute instance elastic-ip attach nonexistent-e2e-instance 1.2.3.4 +stderr 'not found in zone' +stderr 'Hint: use -z' + +# elastic-ip detach (requires instance + eip) +! exec exo compute instance elastic-ip detach nonexistent-e2e-instance 1.2.3.4 +stderr 'not found in zone' +stderr 'Hint: use -z' + +# ssh (requires instance; will fail at instance lookup before attempting SSH) +! exec exo compute instance ssh nonexistent-e2e-instance +stderr 'not found in zone' +stderr 'Hint: use -z' + +# scp (requires instance + source + target) +! exec exo compute instance scp nonexistent-e2e-instance /dev/null /tmp/dummy +stderr 'not found in zone' +stderr 'Hint: use -z'