diff --git a/cmd/unikraft/instances_test.go b/cmd/unikraft/instances_test.go index 1ce3f56e..5ceb3fbb 100644 --- a/cmd/unikraft/instances_test.go +++ b/cmd/unikraft/instances_test.go @@ -306,6 +306,33 @@ func instancesTests(t *testing.T, r *testRunner) { }) }) + t.Run("rm", func(t *testing.T) { + r. + online(). + withCleaners(instanceCleaners). + run(t, []command{ + // Create a running instance with --rm so it is auto-deleted + // when stopped. + {args: []string{ + unikraftCmd, "instance", "create", + "--output", "quiet", + "--rm", + "--set", "name=test-$UNIQ_INST", + "--set", "metro=" + metroName, + "--set", "image=nginx:latest", + "--set", "autostart=true", + "--set", "resources.memory=128", + "--set", "resources.vcpus=1", + }}, + {args: []string{unikraftCmd, "instance", "wait", "--until", "state==running", "--timeout", "30s", "test-$UNIQ_INST"}}, + // Stop the instance — delete-on-stop removes it and the + // diff should show everything being deleted. + {args: []string{unikraftCmd, "instance", "stop", "test-$UNIQ_INST"}}, + // Verify the instance no longer exists. + {args: []string{unikraftCmd, "instance", "inspect", "test-$UNIQ_INST"}, allowErr: true}, + }) + }) + t.Run("add-domain", func(t *testing.T) { r. online(). diff --git a/cmd/unikraft/testdata/TestGolden/instances/help b/cmd/unikraft/testdata/TestGolden/instances/help index 944cd265..eb805a48 100644 --- a/cmd/unikraft/testdata/TestGolden/instances/help +++ b/cmd/unikraft/testdata/TestGolden/instances/help @@ -425,6 +425,8 @@ stdout: Instance features. --template= Create from instance template. + --rm + Automatically delete the instance when it stops. $ unikraft instance edit --help diff --git a/cmd/unikraft/testdata/TestGolden/instances/rm b/cmd/unikraft/testdata/TestGolden/instances/rm new file mode 100644 index 00000000..911985b0 --- /dev/null +++ b/cmd/unikraft/testdata/TestGolden/instances/rm @@ -0,0 +1,50 @@ +$ unikraft instance create --output quiet --rm --set name=test-$UNIQ_INST --set metro=test --set image=nginx:latest --set autostart=true --set resources.memory=128 --set resources.vcpus=1 + +stdout: + test/test- + +$ unikraft instance wait --until state==running --timeout 30s test-$UNIQ_INST + +stdout: + metro: test + name: test- + uuid: 12345678-1234-1234-1234-123456789abc + state: running + image: nginx + resources: + memory: 128MiB + vcpus: 1 + networks: + - uuid: 12345678-1234-1234-1234-123456789abc + private-ip: 10.X.X.X + mac: aa:bb:cc:dd:ee:ff + timestamps: + created: RELATIVE_TIME + +$ unikraft instance stop test-$UNIQ_INST + +stdout: + - metro: test + - name: test- + - uuid: 12345678-1234-1234-1234-123456789abc + - state: running + - image: nginx + - resources: + - memory: 128MiB + - vcpus: 1 + - networks: + - - uuid: 12345678-1234-1234-1234-123456789abc + - private-ip: 10.X.X.X + - mac: aa:bb:cc:dd:ee:ff + - timestamps: + - created: RELATIVE_TIME + +$ unikraft instance inspect test-$UNIQ_INST + +stderr: + │ + │ error: + │ references not found: [test-] + │ + +exit code: 1 diff --git a/internal/cmd/instances.go b/internal/cmd/instances.go index 72124759..4a849532 100644 --- a/internal/cmd/instances.go +++ b/internal/cmd/instances.go @@ -89,12 +89,16 @@ type InstanceCreateCmd struct { Replicas int64 `group:"flag-create" shortcut:"replicas" help:"Number of replicas." placeholder:"n" example:"1,3"` Features []string `group:"flag-create" shortcut:"features" help:"Instance features." placeholder:"feature"` Template string `group:"flag-create" shortcut:"template" help:"Create from instance template." placeholder:"name"` + Rm bool `group:"flag-create" help:"Automatically delete the instance when it stops."` } func (c *InstanceCreateCmd) Run(ctx context.Context, stdio config.Stdio, sandbox *resource.Sandbox, kctx *kong.Context) error { if err := cmd.ApplyShortcutFlags(&c.SetArgs, kctx.Flags()); err != nil { return err } + if c.Rm { + c.Set = append(c.Set, map[string]string{"features": string(platform.CreateInstanceRequestFeaturesDelete_on_stop)}) + } return c.ResourceCreateCmd.Run(ctx, stdio, sandbox) } @@ -1183,9 +1187,17 @@ func (c *InstancesStopCmd) Run(ctx context.Context, stdio config.Stdio) error { } updated, getErr := Instance{}.Get(ctx, stopped.Strings()) - opErr = errors.Join(opErr, getErr) + // If the stopped instances are not found (e.g. removed by + // delete-on-stop), show a diff against an empty "after" state + // instead of returning an error. if getErr != nil && len(updated) == 0 { - return opErr + var refErr group.ErrRefNotFound + if !errors.As(getErr, &refErr) { + opErr = errors.Join(opErr, getErr) + return opErr + } + } else { + opErr = errors.Join(opErr, getErr) } keySet := make(map[string]struct{}, len(stopped)) @@ -1252,14 +1264,27 @@ func (c *InstancesRestartCmd) Run(ctx context.Context, stdio config.Stdio) error started, startErr := startInstances(ctx, g, stopped) opErr = errors.Join(opErr, startErr) + // If all stopped instances were removed (e.g. by delete-on-stop) + // before they could be restarted, show a diff reflecting the + // deletion instead of returning a confusing error. if len(started) == 0 { - return opErr + var refErr group.ErrRefNotFound + if startErr == nil || !errors.As(startErr, &refErr) { + return opErr + } } updated, getErr := Instance{}.Get(ctx, started.Strings()) - opErr = errors.Join(opErr, getErr) + // Same as above: tolerate not-found when delete-on-stop already + // removed the instances. if getErr != nil && len(updated) == 0 { - return opErr + var refErr group.ErrRefNotFound + if !errors.As(getErr, &refErr) { + opErr = errors.Join(opErr, getErr) + return opErr + } + } else { + opErr = errors.Join(opErr, getErr) } keySet := make(map[string]struct{}, len(started))