This repository has no description
1//go:build linux
2
3package microvm
4
5import (
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "strings"
10 "testing"
11)
12
13func writeSpecFile(t *testing.T, path string) {
14 t.Helper()
15 data, err := json.Marshal(validImageSpec())
16 if err != nil {
17 t.Fatal(err)
18 }
19 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
20 t.Fatal(err)
21 }
22 if err := os.WriteFile(path, data, 0o644); err != nil {
23 t.Fatal(err)
24 }
25}
26
27func TestResolveImageConventionalLayouts(t *testing.T) {
28 cases := []struct {
29 name string
30 layout func(t *testing.T, dir string)
31 }{
32 {
33 name: "directory with spec.json",
34 layout: func(t *testing.T, dir string) {
35 writeSpecFile(t, filepath.Join(dir, "nixos", "spec.json"))
36 },
37 },
38 {
39 name: "flat <name>.json",
40 layout: func(t *testing.T, dir string) {
41 writeSpecFile(t, filepath.Join(dir, "nixos.json"))
42 },
43 },
44 }
45
46 for _, tc := range cases {
47 t.Run(tc.name, func(t *testing.T) {
48 dir := t.TempDir()
49 tc.layout(t, dir)
50
51 e := testEngine(t, dir)
52 spec, path, name, err := e.resolveImage("nixos")
53 if err != nil {
54 t.Fatalf("resolveImage: %v", err)
55 }
56 if name != "nixos" {
57 t.Fatalf("name = %q, want nixos", name)
58 }
59 if !strings.HasPrefix(path, dir) {
60 t.Fatalf("resolved path %q not under image dir %q", path, dir)
61 }
62 if spec.Shell == "" {
63 t.Fatal("resolved spec not loaded")
64 }
65 })
66 }
67}
68func TestImageSpecPathPinsSymlinkTarget(t *testing.T) {
69 dir := t.TempDir()
70 imageA := filepath.Join(dir, "image-a")
71 imageB := filepath.Join(dir, "image-b")
72 writeSpecFile(t, filepath.Join(imageA, imageSpecFileName))
73 writeSpecFile(t, filepath.Join(imageB, imageSpecFileName))
74
75 alias := filepath.Join(dir, "default")
76 if err := os.Symlink(imageA, alias); err != nil {
77 t.Fatal(err)
78 }
79 got, ok, err := imageSpecPath(alias)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if !ok {
84 t.Fatal("imageSpecPath() did not resolve symlinked image")
85 }
86
87 if err := os.Remove(alias); err != nil {
88 t.Fatal(err)
89 }
90 if err := os.Symlink(imageB, alias); err != nil {
91 t.Fatal(err)
92 }
93 want := filepath.Join(imageA, imageSpecFileName)
94 if got != want {
95 t.Fatalf("imageSpecPath() = %q after alias resolution, want pinned target %q", got, want)
96 }
97}
98
99func TestResolveImageDirectoryMissingSpec(t *testing.T) {
100 dir := t.TempDir()
101 if err := os.MkdirAll(filepath.Join(dir, "nixos"), 0o755); err != nil {
102 t.Fatal(err)
103 }
104
105 e := testEngine(t, dir)
106 _, _, _, err := e.resolveImage("nixos")
107 if err == nil || !strings.Contains(err.Error(), imageSpecFileName) {
108 t.Fatalf("directory without %s should error, got: %v", imageSpecFileName, err)
109 }
110}
111
112func TestResolveImageRequiresImageDirOnlyWhenUsed(t *testing.T) {
113 e := testEngine(t, "")
114 _, _, _, err := e.resolveImage("nixos")
115 if err == nil || !strings.Contains(err.Error(), "SPINDLE_MICROVM_PIPELINES_IMAGE_DIR") {
116 t.Fatalf("missing image directory should error when resolving an image, got: %v", err)
117 }
118}
119
120func TestResolveImageRejectsPaths(t *testing.T) {
121 e := testEngine(t, t.TempDir())
122 for _, name := range []string{"/etc/passwd", "../evil", "sub/evil", "..", "."} {
123 if _, _, _, err := e.resolveImage(name); err == nil || !strings.Contains(err.Error(), "must be a plain name") {
124 t.Fatalf("name %q should be rejected as a path, got: %v", name, err)
125 }
126 }
127}
128
129func validImageSpec() ImageSpec {
130 return ImageSpec{
131 Arch: "x86_64",
132 BootArgs: "console=ttyS0",
133 Initrd: "initrd",
134 Kernel: "kernel",
135 RunnerConfig: RunnerConfig{
136 Machine: "microvm",
137 },
138 MemoryMiB: 2048,
139 Shell: "/bin/sh",
140 StoreDisk: "store-disk",
141 VCPUs: 2,
142 }
143}
144
145func TestImageSpecValidateWithoutBaseConfigHash(t *testing.T) {
146 spec := validImageSpec()
147 if err := spec.Validate(); err != nil {
148 t.Fatalf("non-NixOS image spec should validate: %v", err)
149 }
150 if spec.SupportsConfigActivation() {
151 t.Fatal("spec without baseConfigHash should not support config activation")
152 }
153
154 spec.BaseConfigHash = "abcdef"
155 if !spec.SupportsConfigActivation() {
156 t.Fatal("spec with baseConfigHash should support config activation")
157 }
158}
159
160func TestImageSpecRequiresShell(t *testing.T) {
161 spec := validImageSpec()
162 spec.Shell = ""
163 err := spec.Validate()
164 if err == nil || !strings.Contains(err.Error(), "shell") {
165 t.Fatalf("spec without shell should fail validation, got: %v", err)
166 }
167}
168func TestMkfsExt4ForVolumesSkipsLookupWithoutVolumes(t *testing.T) {
169 t.Setenv("PATH", "")
170 path, err := mkfsExt4ForVolumes(nil, "")
171 if err != nil {
172 t.Fatalf("mkfsExt4ForVolumes() with no volumes returned error: %v", err)
173 }
174 if path != "" {
175 t.Fatalf("mkfsExt4ForVolumes() = %q with no volumes, want empty path", path)
176 }
177
178 _, err = mkfsExt4ForVolumes([]Volume{{Image: "workspace"}}, "")
179 if err == nil || !strings.Contains(err.Error(), "mkfs.ext4") {
180 t.Fatalf("mkfsExt4ForVolumes() with a volume and no formatter returned %v", err)
181 }
182}
183
184func TestQEMURunnerValidateRejectsUnsupportedNetworkTypeBeforeHostChecks(t *testing.T) {
185 spec := validImageSpec()
186 spec.NetworkInterfaces = []NetworkInterface{{
187 Type: "tap",
188 ID: "net0",
189 MAC: "02:00:00:00:00:01",
190 }}
191 err := (qemuRunner{}).Validate(spec, false)
192 if err == nil || !strings.Contains(err.Error(), `unsupported microvm network interface type "tap"`) {
193 t.Fatalf("Validate() error = %v, want unsupported network type", err)
194 }
195}