-
Notifications
You must be signed in to change notification settings - Fork 1
test: add tart VM lifecycle integration tests #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
660b772
e7ff8ca
45b7b78
90f9e15
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| Feature: Tart VM lifecycle | ||
|
|
||
| The tart manager wraps the tart CLI for VM operations: pull, clone, | ||
| start, IP discovery, SSH exec, stop, and delete. These tests require | ||
| a real tart installation with nested virtualization enabled. | ||
|
|
||
| Scenario: pull, clone, start, exec, and cleanup a VM | ||
| Given a tart manager | ||
| When I pull the VM image | ||
| Then the VM image should exist locally | ||
| When I clone a VM with a random name | ||
| And I start the cloned VM | ||
| And I wait for the VM IP address | ||
| Then the VM IP should be a valid address | ||
| When I exec "echo hello" in the VM | ||
| Then the exec should succeed | ||
| When I stop and delete the VM | ||
| Then the VM should no longer exist | ||
|
|
||
| Scenario: list and cleanup orphaned VMs | ||
| Given a tart manager | ||
| When I clone a VM with a random name | ||
| And I start the cloned VM | ||
| Then listing local VMs should include the cloned VM | ||
| When I cleanup all VMs with the test prefix | ||
| Then listing local VMs should not include the cloned VM | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ import ( | |
| "github.com/boring-design/elastic-fruit-runner/internal/binpath" | ||
| "github.com/boring-design/elastic-fruit-runner/internal/management" | ||
| "github.com/boring-design/elastic-fruit-runner/internal/management/migrations" | ||
| "github.com/boring-design/elastic-fruit-runner/internal/tart" | ||
| "github.com/boring-design/elastic-fruit-runner/internal/vitals" | ||
| ) | ||
|
|
||
|
|
@@ -67,6 +68,12 @@ type scenarioState struct { | |
| workflowResult *github.WorkflowRun | ||
| runnerSetsResp *controlplanev1.ListRunnerSetsResponse | ||
| jobRecordsResp *controlplanev1.ListJobRecordsResponse | ||
|
|
||
| // tart steps | ||
| tartMgr *tart.Manager | ||
| tartVMName string | ||
| tartVMIP string | ||
| tartPrefix string | ||
| } | ||
|
|
||
| func initializeScenario(sc *godog.ScenarioContext) { | ||
|
|
@@ -649,6 +656,128 @@ func initializeScenario(sc *godog.ScenarioContext) { | |
| state.mgmtService.Close() | ||
| } | ||
| }) | ||
|
|
||
| // ---- Tart VM steps ---- | ||
| sc.Step(`^a tart manager$`, func(ctx context.Context) (context.Context, error) { | ||
| if binpath.Lookup("tart") == "tart" { | ||
| return ctx, godog.ErrPending | ||
| } | ||
| state.tartMgr = tart.NewManager() | ||
| state.tartPrefix = "efr-tart-test" | ||
| return ctx, nil | ||
| }) | ||
|
|
||
| sc.Step(`^I pull the VM image$`, func() error { | ||
| image := envOrDefault("EFR_TEST_TART_IMAGE", "ghcr.io/cirruslabs/macos-tahoe-base:latest") | ||
| return state.tartMgr.Pull(context.Background(), image) | ||
| }) | ||
|
|
||
| sc.Step(`^the VM image should exist locally$`, func() error { | ||
| image := envOrDefault("EFR_TEST_TART_IMAGE", "ghcr.io/cirruslabs/macos-tahoe-base:latest") | ||
| exists, err := state.tartMgr.ImageExists(context.Background(), image) | ||
| if err != nil { | ||
| return fmt.Errorf("check image exists: %w", err) | ||
| } | ||
| if !exists { | ||
| return fmt.Errorf("image %q not found locally after pull", image) | ||
| } | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^I clone a VM with a random name$`, func() error { | ||
| image := envOrDefault("EFR_TEST_TART_IMAGE", "ghcr.io/cirruslabs/macos-tahoe-base:latest") | ||
| state.tartVMName = state.tartPrefix + "-" + randomSuffix() | ||
| return state.tartMgr.Clone(context.Background(), image, state.tartVMName) | ||
| }) | ||
|
|
||
| sc.Step(`^I start the cloned VM$`, func() error { | ||
| return state.tartMgr.Start(context.Background(), state.tartVMName) | ||
| }) | ||
|
|
||
| sc.Step(`^I wait for the VM IP address$`, func() error { | ||
| ip, err := state.tartMgr.IPAddress(context.Background(), state.tartVMName) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| state.tartVMIP = ip | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^the VM IP should be a valid address$`, func() error { | ||
| if net.ParseIP(state.tartVMIP) == nil { | ||
| return fmt.Errorf("invalid IP address: %q", state.tartVMIP) | ||
| } | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^I exec "([^"]*)" in the VM$`, func(cmd string) error { | ||
| return state.tartMgr.Exec(context.Background(), state.tartVMName, "bash", "-c", cmd) | ||
| }) | ||
|
|
||
| sc.Step(`^the exec should succeed$`, func() error { | ||
| // The step above already returns error on failure | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^I stop and delete the VM$`, func() error { | ||
| if err := state.tartMgr.Stop(context.Background(), state.tartVMName); err != nil { | ||
| return fmt.Errorf("stop VM: %w", err) | ||
| } | ||
| return state.tartMgr.Delete(context.Background(), state.tartVMName) | ||
| }) | ||
|
|
||
| sc.Step(`^the VM should no longer exist$`, func() error { | ||
| vms, err := state.tartMgr.List(context.Background()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, name := range vms { | ||
| if name == state.tartVMName { | ||
| return fmt.Errorf("VM %q still exists after delete", state.tartVMName) | ||
| } | ||
| } | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^listing local VMs should include the cloned VM$`, func() error { | ||
| vms, err := state.tartMgr.List(context.Background()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, name := range vms { | ||
| if name == state.tartVMName { | ||
| return nil | ||
| } | ||
| } | ||
| return fmt.Errorf("VM %q not found in list", state.tartVMName) | ||
| }) | ||
|
|
||
| sc.Step(`^I cleanup all VMs with the test prefix$`, func() error { | ||
| vms, err := state.tartMgr.List(context.Background()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, name := range vms { | ||
| if strings.HasPrefix(name, state.tartPrefix+"-") { | ||
| _ = state.tartMgr.Stop(context.Background(), name) | ||
| _ = state.tartMgr.Delete(context.Background(), name) | ||
|
Comment on lines
+762
to
+763
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This step ignores both Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
| return nil | ||
| }) | ||
|
|
||
| sc.Step(`^listing local VMs should not include the cloned VM$`, func() error { | ||
| vms, err := state.tartMgr.List(context.Background()) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| for _, name := range vms { | ||
| if name == state.tartVMName { | ||
| return fmt.Errorf("VM %q still exists after cleanup", state.tartVMName) | ||
| } | ||
| } | ||
| return nil | ||
| }) | ||
| } | ||
|
|
||
| // buildMgmtConfig creates a management service config from env vars with the given auth. | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
list and cleanup orphaned VMsscenario clones immediately without first ensuring the test image is present locally, so on a clean runner (or when this scenario is run in isolation)Clonecan fail before any cleanup assertions run. This makes the scenario order-dependent onpull, clone, start, exec, and cleanup a VMinstead of being self-contained.Useful? React with 👍 / 👎.