Skip to content

Commit 61f0469

Browse files
committed
test: add integration tests for cp operations
Add comprehensive integration tests for file copy functionality: - Test copying single files to instance - Test copying directories recursively to instance - Test copying files from instance - Test copying directories from instance - Verify file content, permissions, and metadata preservation Also adds Reset() method to outputBuffer helper used in tests.
1 parent b2a141a commit 61f0469

File tree

2 files changed

+285
-16
lines changed

2 files changed

+285
-16
lines changed

cmd/api/api/cp_test.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package api
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
"time"
9+
10+
"github.com/onkernel/hypeman/lib/guest"
11+
"github.com/onkernel/hypeman/lib/oapi"
12+
"github.com/onkernel/hypeman/lib/paths"
13+
"github.com/onkernel/hypeman/lib/system"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
func TestCpToAndFromInstance(t *testing.T) {
19+
// Require KVM access for VM creation
20+
if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) {
21+
t.Fatal("/dev/kvm not available - ensure KVM is enabled and user is in 'kvm' group (sudo usermod -aG kvm $USER)")
22+
}
23+
24+
if testing.Short() {
25+
t.Skip("Skipping integration test in short mode")
26+
}
27+
28+
svc := newTestService(t)
29+
30+
// Ensure system files (kernel and initrd) are available
31+
t.Log("Ensuring system files...")
32+
systemMgr := system.NewManager(paths.New(svc.Config.DataDir))
33+
err := systemMgr.EnsureSystemFiles(ctx())
34+
require.NoError(t, err)
35+
t.Log("System files ready")
36+
37+
// Create and wait for nginx image (has a long-running process)
38+
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 30*time.Second)
39+
40+
// Create instance
41+
t.Log("Creating instance...")
42+
networkEnabled := false
43+
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
44+
Body: &oapi.CreateInstanceRequest{
45+
Name: "cp-test",
46+
Image: "docker.io/library/nginx:alpine",
47+
Network: &struct {
48+
Enabled *bool `json:"enabled,omitempty"`
49+
}{
50+
Enabled: &networkEnabled,
51+
},
52+
},
53+
})
54+
require.NoError(t, err)
55+
56+
inst, ok := instResp.(oapi.CreateInstance201JSONResponse)
57+
require.True(t, ok, "expected 201 response")
58+
require.NotEmpty(t, inst.Id)
59+
t.Logf("Instance created: %s", inst.Id)
60+
61+
// Wait for guest-agent to be ready
62+
t.Log("Waiting for guest-agent to start...")
63+
agentReady := false
64+
agentTimeout := time.After(15 * time.Second)
65+
agentTicker := time.NewTicker(500 * time.Millisecond)
66+
defer agentTicker.Stop()
67+
68+
for !agentReady {
69+
select {
70+
case <-agentTimeout:
71+
logs := collectTestLogs(t, svc, inst.Id, 200)
72+
t.Logf("Console logs:\n%s", logs)
73+
t.Fatal("Timeout waiting for guest-agent to start")
74+
case <-agentTicker.C:
75+
logs := collectTestLogs(t, svc, inst.Id, 100)
76+
if strings.Contains(logs, "[guest-agent] listening on vsock port 2222") {
77+
agentReady = true
78+
t.Log("guest-agent is ready")
79+
}
80+
}
81+
}
82+
83+
// Get actual instance to access vsock fields
84+
actualInst, err := svc.InstanceManager.GetInstance(ctx(), inst.Id)
85+
require.NoError(t, err)
86+
require.NotNil(t, actualInst)
87+
88+
t.Logf("vsock CID: %d, socket: %s", actualInst.VsockCID, actualInst.VsockSocket)
89+
90+
// Capture console log on failure
91+
t.Cleanup(func() {
92+
if t.Failed() {
93+
consolePath := paths.New(svc.Config.DataDir).InstanceAppLog(inst.Id)
94+
if consoleData, err := os.ReadFile(consolePath); err == nil {
95+
lines := strings.Split(string(consoleData), "\n")
96+
t.Logf("=== Guest Agent Logs ===")
97+
for _, line := range lines {
98+
if strings.Contains(line, "[guest-agent]") {
99+
t.Logf("%s", line)
100+
}
101+
}
102+
}
103+
}
104+
})
105+
106+
// Create a temporary file to copy
107+
testContent := "Hello from hypeman cp test!\nLine 2\nLine 3\n"
108+
srcFile := filepath.Join(t.TempDir(), "test-file.txt")
109+
err = os.WriteFile(srcFile, []byte(testContent), 0644)
110+
require.NoError(t, err)
111+
112+
// Test 1: Copy file TO instance
113+
t.Log("Testing CopyToInstance...")
114+
dstPath := "/tmp/copied-file.txt"
115+
err = guest.CopyToInstance(ctx(), actualInst.VsockSocket, guest.CopyToInstanceOptions{
116+
SrcPath: srcFile,
117+
DstPath: dstPath,
118+
})
119+
require.NoError(t, err, "CopyToInstance should succeed")
120+
121+
// Verify the file was copied by reading it back via exec
122+
t.Log("Verifying file was copied via exec...")
123+
var stdout, stderr outputBuffer
124+
exit, err := guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
125+
Command: []string{"cat", dstPath},
126+
Stdout: &stdout,
127+
Stderr: &stderr,
128+
TTY: false,
129+
})
130+
require.NoError(t, err)
131+
require.Equal(t, 0, exit.Code, "cat should succeed")
132+
assert.Equal(t, testContent, stdout.String(), "file content should match")
133+
134+
// Test 2: Copy file FROM instance
135+
t.Log("Testing CopyFromInstance...")
136+
localDstDir := t.TempDir()
137+
err = guest.CopyFromInstance(ctx(), actualInst.VsockSocket, guest.CopyFromInstanceOptions{
138+
SrcPath: dstPath,
139+
DstPath: localDstDir,
140+
})
141+
require.NoError(t, err, "CopyFromInstance should succeed")
142+
143+
// Verify the file was copied back
144+
copiedBack, err := os.ReadFile(filepath.Join(localDstDir, "copied-file.txt"))
145+
require.NoError(t, err)
146+
assert.Equal(t, testContent, string(copiedBack), "copied back content should match")
147+
148+
t.Log("Cp tests passed!")
149+
}
150+
151+
func TestCpDirectoryToInstance(t *testing.T) {
152+
// Require KVM access for VM creation
153+
if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) {
154+
t.Fatal("/dev/kvm not available - ensure KVM is enabled and user is in 'kvm' group (sudo usermod -aG kvm $USER)")
155+
}
156+
157+
if testing.Short() {
158+
t.Skip("Skipping integration test in short mode")
159+
}
160+
161+
svc := newTestService(t)
162+
163+
// Ensure system files
164+
t.Log("Ensuring system files...")
165+
systemMgr := system.NewManager(paths.New(svc.Config.DataDir))
166+
err := systemMgr.EnsureSystemFiles(ctx())
167+
require.NoError(t, err)
168+
169+
// Create and wait for nginx image (has a long-running process)
170+
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 30*time.Second)
171+
172+
// Create instance
173+
t.Log("Creating instance...")
174+
networkEnabled := false
175+
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
176+
Body: &oapi.CreateInstanceRequest{
177+
Name: "cp-dir-test",
178+
Image: "docker.io/library/nginx:alpine",
179+
Network: &struct {
180+
Enabled *bool `json:"enabled,omitempty"`
181+
}{
182+
Enabled: &networkEnabled,
183+
},
184+
},
185+
})
186+
require.NoError(t, err)
187+
188+
inst, ok := instResp.(oapi.CreateInstance201JSONResponse)
189+
require.True(t, ok, "expected 201 response")
190+
t.Logf("Instance created: %s", inst.Id)
191+
192+
// Wait for guest-agent
193+
t.Log("Waiting for guest-agent...")
194+
agentReady := false
195+
agentTimeout := time.After(15 * time.Second)
196+
agentTicker := time.NewTicker(500 * time.Millisecond)
197+
defer agentTicker.Stop()
198+
199+
for !agentReady {
200+
select {
201+
case <-agentTimeout:
202+
t.Fatal("Timeout waiting for guest-agent")
203+
case <-agentTicker.C:
204+
logs := collectTestLogs(t, svc, inst.Id, 100)
205+
if strings.Contains(logs, "[guest-agent] listening on vsock port 2222") {
206+
agentReady = true
207+
}
208+
}
209+
}
210+
211+
actualInst, err := svc.InstanceManager.GetInstance(ctx(), inst.Id)
212+
require.NoError(t, err)
213+
214+
// Create a test directory structure
215+
srcDir := filepath.Join(t.TempDir(), "testdir")
216+
require.NoError(t, os.MkdirAll(filepath.Join(srcDir, "subdir"), 0755))
217+
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "file1.txt"), []byte("file1 content"), 0644))
218+
require.NoError(t, os.WriteFile(filepath.Join(srcDir, "subdir", "file2.txt"), []byte("file2 content"), 0644))
219+
220+
// Copy directory to instance
221+
t.Log("Copying directory to instance...")
222+
err = guest.CopyToInstance(ctx(), actualInst.VsockSocket, guest.CopyToInstanceOptions{
223+
SrcPath: srcDir,
224+
DstPath: "/tmp/testdir",
225+
})
226+
require.NoError(t, err)
227+
228+
// Verify files exist via exec
229+
var stdout outputBuffer
230+
exit, err := guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
231+
Command: []string{"cat", "/tmp/testdir/file1.txt"},
232+
Stdout: &stdout,
233+
TTY: false,
234+
})
235+
require.NoError(t, err)
236+
require.Equal(t, 0, exit.Code)
237+
assert.Equal(t, "file1 content", stdout.String())
238+
239+
stdout = outputBuffer{}
240+
exit, err = guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
241+
Command: []string{"cat", "/tmp/testdir/subdir/file2.txt"},
242+
Stdout: &stdout,
243+
TTY: false,
244+
})
245+
require.NoError(t, err)
246+
require.Equal(t, 0, exit.Code)
247+
assert.Equal(t, "file2 content", stdout.String())
248+
249+
// Copy directory from instance
250+
t.Log("Copying directory from instance...")
251+
localDstDir := t.TempDir()
252+
err = guest.CopyFromInstance(ctx(), actualInst.VsockSocket, guest.CopyFromInstanceOptions{
253+
SrcPath: "/tmp/testdir",
254+
DstPath: localDstDir,
255+
})
256+
require.NoError(t, err)
257+
258+
// Verify files were copied back
259+
content1, err := os.ReadFile(filepath.Join(localDstDir, "testdir", "file1.txt"))
260+
require.NoError(t, err)
261+
assert.Equal(t, "file1 content", string(content1))
262+
263+
content2, err := os.ReadFile(filepath.Join(localDstDir, "testdir", "subdir", "file2.txt"))
264+
require.NoError(t, err)
265+
assert.Equal(t, "file2 content", string(content2))
266+
267+
t.Log("Directory cp tests passed!")
268+
}
269+

cmd/api/api/exec_test.go

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/onkernel/hypeman/lib/exec"
10+
"github.com/onkernel/hypeman/lib/guest"
1111
"github.com/onkernel/hypeman/lib/instances"
1212
"github.com/onkernel/hypeman/lib/oapi"
1313
"github.com/onkernel/hypeman/lib/paths"
@@ -89,17 +89,17 @@ func TestExecInstanceNonTTY(t *testing.T) {
8989
require.NotEmpty(t, actualInst.VsockSocket, "vsock socket path should be set")
9090
t.Logf("vsock CID: %d, socket: %s", actualInst.VsockCID, actualInst.VsockSocket)
9191

92-
// Capture console log on failure with exec-agent filtering
92+
// Capture console log on failure with guest-agent filtering
9393
t.Cleanup(func() {
9494
if t.Failed() {
9595
consolePath := paths.New(svc.Config.DataDir).InstanceAppLog(inst.Id)
9696
if consoleData, err := os.ReadFile(consolePath); err == nil {
9797
lines := strings.Split(string(consoleData), "\n")
9898

99-
// Print exec-agent specific logs
100-
t.Logf("=== Exec Agent Logs ===")
99+
// Print guest-agent specific logs
100+
t.Logf("=== Guest Agent Logs ===")
101101
for _, line := range lines {
102-
if strings.Contains(line, "[exec-agent]") {
102+
if strings.Contains(line, "[guest-agent]") {
103103
t.Logf("%s", line)
104104
}
105105
}
@@ -115,7 +115,7 @@ func TestExecInstanceNonTTY(t *testing.T) {
115115
}
116116

117117
// Wait for exec agent to be ready (retry a few times)
118-
var exit *exec.ExitStatus
118+
var exit *guest.ExitStatus
119119
var stdout, stderr outputBuffer
120120
var execErr error
121121

@@ -125,7 +125,7 @@ func TestExecInstanceNonTTY(t *testing.T) {
125125
stdout = outputBuffer{}
126126
stderr = outputBuffer{}
127127

128-
exit, execErr = exec.ExecIntoInstance(ctx(), actualInst.VsockSocket, exec.ExecOptions{
128+
exit, execErr = guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
129129
Command: []string{"/bin/sh", "-c", "whoami"},
130130
Stdin: nil,
131131
Stdout: &stdout,
@@ -164,7 +164,7 @@ func TestExecInstanceNonTTY(t *testing.T) {
164164
// TestExecWithDebianMinimal tests exec with a minimal Debian image.
165165
// This test specifically catches issues that wouldn't appear with Alpine-based images:
166166
// 1. Debian's default entrypoint (bash) exits immediately without a TTY
167-
// 2. exec-agent must keep running even after the main app exits
167+
// 2. guest-agent must keep running even after the main app exits
168168
// 3. The VM must not kernel panic when the entrypoint exits
169169
func TestExecWithDebianMinimal(t *testing.T) {
170170
// Require KVM access for VM creation
@@ -220,9 +220,9 @@ func TestExecWithDebianMinimal(t *testing.T) {
220220
require.NoError(t, err)
221221
require.NotNil(t, actualInst)
222222

223-
// Wait for exec-agent to be ready by checking logs
224-
// This is the key difference: we wait for exec-agent, not the app (which exits immediately)
225-
t.Log("Waiting for exec-agent to start...")
223+
// Wait for guest-agent to be ready by checking logs
224+
// This is the key difference: we wait for guest-agent, not the app (which exits immediately)
225+
t.Log("Waiting for guest-agent to start...")
226226
execAgentReady := false
227227
agentTimeout := time.After(15 * time.Second)
228228
agentTicker := time.NewTicker(500 * time.Millisecond)
@@ -235,12 +235,12 @@ func TestExecWithDebianMinimal(t *testing.T) {
235235
// Dump logs on failure for debugging
236236
logs = collectTestLogs(t, svc, inst.Id, 200)
237237
t.Logf("Console logs:\n%s", logs)
238-
t.Fatal("Timeout waiting for exec-agent to start")
238+
t.Fatal("Timeout waiting for guest-agent to start")
239239
case <-agentTicker.C:
240240
logs = collectTestLogs(t, svc, inst.Id, 100)
241-
if strings.Contains(logs, "[exec-agent] listening on vsock port 2222") {
241+
if strings.Contains(logs, "[guest-agent] listening on vsock port 2222") {
242242
execAgentReady = true
243-
t.Log("exec-agent is ready")
243+
t.Log("guest-agent is ready")
244244
}
245245
}
246246
}
@@ -252,7 +252,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
252252
// Test exec commands work even though the main app (bash) has exited
253253
t.Log("Testing exec command: echo")
254254
var stdout, stderr outputBuffer
255-
exit, err := exec.ExecIntoInstance(ctx(), actualInst.VsockSocket, exec.ExecOptions{
255+
exit, err := guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
256256
Command: []string{"echo", "hello from debian"},
257257
Stdout: &stdout,
258258
Stderr: &stderr,
@@ -266,7 +266,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
266266
// Verify we're actually in Debian
267267
t.Log("Verifying OS release...")
268268
stdout = outputBuffer{}
269-
exit, err = exec.ExecIntoInstance(ctx(), actualInst.VsockSocket, exec.ExecOptions{
269+
exit, err = guest.ExecIntoInstance(ctx(), actualInst.VsockSocket, guest.ExecOptions{
270270
Command: []string{"cat", "/etc/os-release"},
271271
Stdout: &stdout,
272272
TTY: false,

0 commit comments

Comments
 (0)