Skip to content

Commit 10fe606

Browse files
committed
feat(agent): add IsAlive and Reattach to VMDriver interface + StubDriver
1 parent c565751 commit 10fe606

4 files changed

Lines changed: 105 additions & 107 deletions

File tree

internal/agent/driver.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,13 @@ type VMDriver interface {
4141
// The VM is always resumed before Snapshot returns, even on error.
4242
// destDir must exist and be writable.
4343
Snapshot(ctx context.Context, vm *impdevv1alpha1.ImpVM, destDir string) (SnapshotResult, error)
44+
45+
// IsAlive returns true if the process with the given PID is still running.
46+
// Uses syscall.Kill(pid, 0) on Linux. StubDriver returns IsAliveResult.
47+
IsAlive(pid int64) bool
48+
49+
// Reattach re-registers an already-running VM (started by a previous agent
50+
// process) into the driver's internal state without launching a new process.
51+
// Called during lazy recovery when the agent restarts and finds a live PID.
52+
Reattach(ctx context.Context, vm *impdevv1alpha1.ImpVM) error
4453
}

internal/agent/firecracker_driver.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ type FirecrackerDriver struct {
8989
procs map[string]*fcProc // keyed by vmKey(vm)
9090
}
9191

92+
// compile-time interface check.
93+
var _ VMDriver = (*FirecrackerDriver)(nil)
94+
9295
// NewFirecrackerDriver constructs a FirecrackerDriver from environment variables:
9396
// - FC_BIN — path to firecracker binary (falls back to exec.LookPath)
9497
// - FC_SOCK_DIR — socket directory (default: /run/imp/sockets)
@@ -639,5 +642,12 @@ func (d *FirecrackerDriver) applySnapshotBoot(ctx context.Context, vm *impdevv1a
639642
return nil
640643
}
641644

642-
// compile-time interface check.
643-
var _ VMDriver = (*FirecrackerDriver)(nil)
645+
// IsAlive is a placeholder — real implementation in Task 2.
646+
func (d *FirecrackerDriver) IsAlive(_ int64) bool {
647+
return false
648+
}
649+
650+
// Reattach is a placeholder — real implementation in Task 2.
651+
func (d *FirecrackerDriver) Reattach(_ context.Context, _ *impdevv1alpha1.ImpVM) error {
652+
return fmt.Errorf("not implemented")
653+
}

internal/agent/stub_driver.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ type StubDriver struct {
2828
startErr error
2929
stopErr error
3030
inspectErr error
31+
32+
// IsAliveResult is returned by IsAlive.
33+
IsAliveResult bool
34+
// ReattachCalls records the vmKey of each Reattach call for test assertions.
35+
ReattachCalls []string
36+
// ReattachErr is returned by Reattach (one-shot, cleared after use).
37+
ReattachErr error
3138
}
3239

3340
// InjectStartError causes the next Start call to return err (one-shot).
@@ -111,3 +118,23 @@ func (d *StubDriver) Snapshot(_ context.Context, vm *impdevv1alpha1.ImpVM, destD
111118
}
112119
return SnapshotResult{StatePath: statePath, MemPath: memPath}, nil
113120
}
121+
122+
// IsAlive returns IsAliveResult. The pid argument is ignored.
123+
func (d *StubDriver) IsAlive(_ int64) bool {
124+
d.mu.Lock()
125+
defer d.mu.Unlock()
126+
return d.IsAliveResult
127+
}
128+
129+
// Reattach records the call and returns ReattachErr (one-shot, cleared after use).
130+
func (d *StubDriver) Reattach(_ context.Context, vm *impdevv1alpha1.ImpVM) error {
131+
d.mu.Lock()
132+
defer d.mu.Unlock()
133+
d.ReattachCalls = append(d.ReattachCalls, vmKey(vm))
134+
if d.ReattachErr != nil {
135+
err := d.ReattachErr
136+
d.ReattachErr = nil
137+
return err
138+
}
139+
return nil
140+
}

internal/agent/stub_driver_test.go

Lines changed: 57 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,118 +1,70 @@
1-
package agent_test
1+
//go:build linux
2+
3+
package agent
24

35
import (
46
"context"
5-
"fmt"
6-
"testing"
7+
"errors"
78

9+
. "github.com/onsi/ginkgo/v2"
10+
. "github.com/onsi/gomega"
811
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
912

1013
impdevv1alpha1 "github.com/syscode-labs/imp/api/v1alpha1"
11-
"github.com/syscode-labs/imp/internal/agent"
1214
)
1315

14-
func TestStubDriver_StartInspectStop(t *testing.T) {
15-
ctx := context.Background()
16-
d := agent.NewStubDriver()
17-
18-
vm := &impdevv1alpha1.ImpVM{
19-
ObjectMeta: metav1.ObjectMeta{Name: "test-vm", Namespace: "default"},
20-
}
21-
22-
// Before Start: Inspect returns not-running.
23-
state, err := d.Inspect(ctx, vm)
24-
if err != nil {
25-
t.Fatalf("Inspect before Start: %v", err)
26-
}
27-
if state.Running {
28-
t.Fatal("expected not running before Start")
29-
}
30-
31-
// Start allocates a PID and IP.
32-
pid, err := d.Start(ctx, vm)
33-
if err != nil {
34-
t.Fatalf("Start: %v", err)
35-
}
36-
if pid <= 0 {
37-
t.Fatalf("expected positive PID, got %d", pid)
38-
}
39-
40-
// Inspect after Start: running=true, IP set, PID matches.
41-
state, err = d.Inspect(ctx, vm)
42-
if err != nil {
43-
t.Fatalf("Inspect after Start: %v", err)
44-
}
45-
if !state.Running {
46-
t.Fatal("expected running after Start")
47-
}
48-
if state.IP == "" {
49-
t.Fatal("expected non-empty IP after Start")
50-
}
51-
if state.PID != pid {
52-
t.Fatalf("expected PID %d, got %d", pid, state.PID)
53-
}
54-
55-
// Stop clears state.
56-
if err := d.Stop(ctx, vm); err != nil {
57-
t.Fatalf("Stop: %v", err)
58-
}
59-
60-
// Inspect after Stop: not running.
61-
state, err = d.Inspect(ctx, vm)
62-
if err != nil {
63-
t.Fatalf("Inspect after Stop: %v", err)
64-
}
65-
if state.Running {
66-
t.Fatal("expected not running after Stop")
67-
}
68-
}
69-
70-
func TestStubDriver_Snapshot_success(t *testing.T) {
71-
d := agent.NewStubDriver()
72-
vm := &impdevv1alpha1.ImpVM{}
73-
vm.Namespace, vm.Name = "ns", "vm1"
74-
_, _ = d.Start(context.Background(), vm)
75-
76-
res, err := d.Snapshot(context.Background(), vm, t.TempDir())
77-
if err != nil {
78-
t.Fatalf("unexpected error: %v", err)
79-
}
80-
if res.StatePath == "" || res.MemPath == "" {
81-
t.Errorf("expected non-empty paths, got %+v", res)
82-
}
83-
}
84-
85-
func TestStubDriver_Snapshot_notRunning(t *testing.T) {
86-
d := agent.NewStubDriver()
87-
vm := &impdevv1alpha1.ImpVM{}
88-
vm.Namespace, vm.Name = "ns", "vm-missing"
89-
90-
_, err := d.Snapshot(context.Background(), vm, t.TempDir())
91-
if err == nil {
92-
t.Error("expected error for non-running VM")
93-
}
94-
}
95-
96-
func TestStubDriver_ConcurrentSafe(t *testing.T) {
97-
ctx := context.Background()
98-
d := agent.NewStubDriver()
99-
100-
done := make(chan struct{})
101-
for i := 0; i < 10; i++ {
102-
go func(i int) {
103-
vm := &impdevv1alpha1.ImpVM{
16+
var _ = Describe("StubDriver: IsAlive and Reattach", func() {
17+
var d *StubDriver
18+
var vm *impdevv1alpha1.ImpVM
19+
20+
BeforeEach(func() {
21+
d = NewStubDriver()
22+
vm = &impdevv1alpha1.ImpVM{
23+
ObjectMeta: metav1.ObjectMeta{
24+
Name: "test-vm",
25+
Namespace: "default",
26+
},
27+
}
28+
})
29+
30+
Describe("IsAlive", func() {
31+
It("returns false when IsAliveResult is false (default)", func() {
32+
Expect(d.IsAlive(int64(12345))).To(BeFalse())
33+
})
34+
35+
It("returns true when IsAliveResult is true", func() {
36+
d.IsAliveResult = true
37+
Expect(d.IsAlive(int64(12345))).To(BeTrue())
38+
})
39+
})
40+
41+
Describe("Reattach", func() {
42+
It("records the vmKey in ReattachCalls", func() {
43+
Expect(d.Reattach(context.Background(), vm)).To(Succeed())
44+
Expect(d.ReattachCalls).To(ConsistOf("default/test-vm"))
45+
})
46+
47+
It("accumulates multiple calls", func() {
48+
vm2 := &impdevv1alpha1.ImpVM{
10449
ObjectMeta: metav1.ObjectMeta{
105-
Name: fmt.Sprintf("vm-%d", i),
50+
Name: "other-vm",
10651
Namespace: "default",
10752
},
10853
}
109-
_, _ = d.Start(ctx, vm)
110-
_, _ = d.Inspect(ctx, vm)
111-
_ = d.Stop(ctx, vm)
112-
done <- struct{}{}
113-
}(i)
114-
}
115-
for i := 0; i < 10; i++ {
116-
<-done
117-
}
118-
}
54+
Expect(d.Reattach(context.Background(), vm)).To(Succeed())
55+
Expect(d.Reattach(context.Background(), vm2)).To(Succeed())
56+
Expect(d.ReattachCalls).To(ConsistOf("default/test-vm", "default/other-vm"))
57+
})
58+
59+
It("returns and clears ReattachErr on error", func() {
60+
sentinel := errors.New("reattach failed")
61+
d.ReattachErr = sentinel
62+
63+
err := d.Reattach(context.Background(), vm)
64+
Expect(err).To(MatchError(sentinel))
65+
66+
// error is cleared — second call succeeds
67+
Expect(d.Reattach(context.Background(), vm)).To(Succeed())
68+
})
69+
})
70+
})

0 commit comments

Comments
 (0)