Skip to content

Commit c90f032

Browse files
author
Shreyansh Sancheti
committed
test: add LCOWV2 feature flag and v2 LCOW test surface
Introduce a `-feature LCOWV2` gate across the functional and cri-containerd test suites so the v2 LCOW controller can be exercised end-to-end without disturbing the existing v1 LCOW pipeline. Functional suite: * LCOWV2 implies LCOW in TestMain so featureLCOW-gated tests are reachable, then defaultLCOWOptions calls requireV1Only to short- circuit every v1 path cleanly. Net effect: only TestLCOW_V2_* runs. * Add helpers_v2_test.go and lcow_v2_test.go covering the v2 surface via internal/builder/vm/lcow + the v2 controller in-process. * Export LCOWBootFilesPath from test/pkg/uvm so v2 tests can resolve boot files without going through v1 *uvm.OptionsLCOW. cri-containerd suite: * Mirror the LCOWV2-implies-LCOW pattern; thread RuntimeHandler onto the CRI ImageSpec when pulling LCOW images so containerd selects the windows-lcow snapshotter and linux/amd64 platform (the sandbox- platform label alone is not honored by containerd >=2.0). * Add lcow_v2_test.go and the runhcs-lcow-v2 runtime handler constant. Flag plumbing: * Add IncludesExplicit and Include to IncludeExcludeStringSet so test TestMain hooks can implement feature implications safely after flag.Parse without breaking default-when-unset semantics. CI: * New `Build and run functional testing binary (LCOWV2)` step that invokes functional.test.exe -feature=LCOWV2. continue-on-error while the v2 surface is being grown. CRI v2 testing is intentionally deferred to a follow-up alongside the integration-tests v2 setup. * Build and upload containerd-shim-lcow-v2.exe as a test artifact. Signed-off-by: Shreyansh Jain <[email protected]>
1 parent 5a0252a commit c90f032

12 files changed

Lines changed: 697 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,56 @@ jobs:
383383
exit $ec
384384
working-directory: test
385385

386+
# Run the v2 LCOW functional tests against the v2 vm.Controller
387+
# end-to-end. The chokepoint in defaultLCOWOptions auto-skips every
388+
# v1 LCOW test under -feature LCOWV2, so this invocation effectively
389+
# runs only the TestLCOW_V2_* surface (and any future v2-aware tests).
390+
# Marked continue-on-error: true while this surface is being grown so
391+
# a v2-specific failure does not block the v1 pipeline.
392+
#
393+
# NOTE: Functional tests build HCS documents directly via
394+
# internal/builder/vm/lcow and create v2 controllers in-process — they
395+
# do NOT exercise the CRI -> containerd -> shim path. CRI v2 testing
396+
# (Test_V2_LCOW_* in test/cri-containerd) requires a separate CI step
397+
# that starts containerd with `snapshotter = "windows-lcow"` set on
398+
# both `runhcs-lcow` AND `runhcs-lcow-v2` runtime blocks (the mapping
399+
# logic lives in the containerd CRI plugin). That step is intentionally
400+
# deferred to a follow-up PR alongside the integration-tests v2 setup.
401+
- name: Build and run functional testing binary (LCOWV2)
402+
continue-on-error: true
403+
run: |
404+
if ( -not (Test-Path './functional.test.exe') ) {
405+
Write-Output '::warning::functional.test.exe missing; skipping LCOWV2 run'
406+
exit 0
407+
}
408+
409+
$gotestsum = Get-Command -Name 'gotestsum' -CommandType Application -ErrorAction 'Stop' |
410+
Select-Object -First 1 -ExpandProperty Source
411+
$go = Get-Command -Name 'go' -CommandType Application -ErrorAction Stop |
412+
Select-Object -First 1 -ExpandProperty Source
413+
414+
# LCOWV2 implies LCOW in TestMain so featureLCOW-gated tests would
415+
# also be reachable, but defaultLCOWOptions calls requireV1Only so
416+
# they skip cleanly. Net effect: only TestLCOW_V2_* tests actually
417+
# execute, exercising the v2 controller end-to-end.
418+
$cmd = '${{ env.GOTESTSUM_CMD_RAW }} ./functional.test.exe -feature=LCOWV2 -exclude=LCOWIntegrity -test.timeout=1h -test.v -log-level=info'
419+
$cmd = $cmd -replace '\bgo\b', $go
420+
$cmd = $cmd -replace '\bgotestsum\b', $gotestsum
421+
Write-Host "gotestsum command: $cmd"
422+
423+
psexec -nobanner -w (Get-Location) -s cmd /c "$cmd > v2-out.txt 2>&1"
424+
$ec = $LASTEXITCODE
425+
426+
Get-Content v2-out.txt
427+
428+
exit $ec
429+
working-directory: test
430+
431+
# Build the v2 LCOW shim binary so it is included in test_binaries
432+
# artifact uploads and available to anyone reproducing v2 test runs.
433+
- name: Build containerd-shim-lcow-v2 binary
434+
run: ${{ env.GO_BUILD_CMD }} -tags lcow -o test/containerd-shim-lcow-v2.exe ./cmd/containerd-shim-lcow-v2
435+
386436
# build testing binaries
387437
- name: Build cri-containerd Testing Binary
388438
run: ${{ env.GO_BUILD_TEST_CMD }} ./cri-containerd
@@ -400,6 +450,7 @@ jobs:
400450
name: test_binaries_${{ matrix.name }}
401451
path: |
402452
test/containerd-shim-runhcs-v1.test.exe
453+
test/containerd-shim-lcow-v2.exe
403454
test/cri-containerd.test.exe
404455
test/functional.test.exe
405456
test/runhcs.test.exe

test/cri-containerd/container_layers_packing_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ func Test_Container_Layer_Packing_On_VPMem(t *testing.T) {
4646
defer cancel()
4747

4848
requireFeatures(t, featureLCOW)
49+
requireV1Only(t)
4950

5051
// use ubuntu to make sure that multiple container layers will be mapped properly
5152
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, ubuntu1804})
@@ -101,6 +102,7 @@ func Test_Many_Container_Layers_Supported_On_VPMem(t *testing.T) {
101102
defer cancel()
102103

103104
requireFeatures(t, featureLCOW)
105+
requireV1Only(t)
104106

105107
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, alpine70ExtraLayers, ubuntu70ExtraLayers})
106108

@@ -132,6 +134,7 @@ func Test_Annotation_Disable_Multi_Mapping(t *testing.T) {
132134
defer cancel()
133135

134136
requireFeatures(t, featureLCOW)
137+
requireV1Only(t)
135138

136139
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, alpine70ExtraLayers})
137140

test/cri-containerd/disable_vpmem_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func uniqueRef() string {
2828

2929
func Test_70LayerImagesWithNoVPmemForLayers(t *testing.T) {
3030
requireFeatures(t, featureLCOW)
31+
requireV1Only(t)
3132

3233
ubuntu70Image := "cplatpublic.azurecr.io/ubuntu70extra:18.04"
3334
alpine70Image := "cplatpublic.azurecr.io/alpine70extra:latest"

test/cri-containerd/helper_sandbox_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,42 @@ import (
1010
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
1111
)
1212

13+
// isLCOWV2 reports whether the LCOWV2 feature flag is set on the current test
14+
// invocation. Callers should prefer the higher-level helpers below
15+
// (lcowRuntimeHandlerForTest, requireV1Only) so the V2 selection logic stays
16+
// in one place.
17+
func isLCOWV2() bool {
18+
return flagFeatures.IsSet(featureLCOWV2)
19+
}
20+
21+
// lcowRuntimeHandlerForTest returns the LCOW runtime handler that the current
22+
// test should target. When the LCOWV2 feature flag is set, it returns the V2
23+
// shim handler (containerd-shim-lcow-v2.exe via runtime_type
24+
// io.containerd.lcow.v2). Otherwise it returns the V1 handler
25+
// (containerd-shim-runhcs-v1.exe via runtime_type io.containerd.runhcs.v1).
26+
//
27+
// Tests that exercise generic LCOW lifecycle and work on both shims should use
28+
// this helper instead of hard-coding lcowRuntimeHandler, so the same suite can
29+
// be run twice in CI: once for V1 (default) and once with -feature LCOWV2 for V2.
30+
// Mirrors the pattern in the azcri repo.
31+
func lcowRuntimeHandlerForTest(tb testing.TB) string {
32+
tb.Helper()
33+
if isLCOWV2() {
34+
return lcowV2RuntimeHandler
35+
}
36+
return lcowRuntimeHandler
37+
}
38+
39+
// requireV1Only skips the test when the LCOWV2 feature flag is set.
40+
// Use this for tests that depend on V1-only features such as VPMEM,
41+
// VHD/initrd boot modes, or other UVM knobs not exposed in the v2 builder.
42+
func requireV1Only(tb testing.TB) {
43+
tb.Helper()
44+
if isLCOWV2() {
45+
tb.Skip("test requires V1 shim features (VPMEM/VHD/initrd) not exposed in V2")
46+
}
47+
}
48+
1349
type SandboxConfigOpt func(*runtime.PodSandboxConfig) error
1450

1551
func WithSandboxAnnotations(annotations map[string]string) SandboxConfigOpt {
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
//go:build windows && functional
2+
// +build windows,functional
3+
4+
// V2 LCOW-specific CRI tests. These mirror the Test_V2Sandbox_* pattern
5+
// established in the azcri test suite: each test gates on featureLCOWV2 and
6+
// targets lcowV2RuntimeHandler directly (no v1 fallback), because the
7+
// scenarios under test exercise the V2 runtime path.
8+
//
9+
// To run these tests:
10+
// 1. The CI containerd config must register `runhcs-lcow-v2` →
11+
// `containerd-shim-lcow-v2.exe` (runtime_type io.containerd.lcow.v2).
12+
// 2. The test binary must be invoked with -feature=LCOWV2.
13+
14+
package cri_containerd
15+
16+
import (
17+
"context"
18+
"testing"
19+
20+
runtime "k8s.io/cri-api/pkg/apis/runtime/v1"
21+
)
22+
23+
// Test_V2_LCOW_PodLifecycle exercises the basic pod-sandbox lifecycle through
24+
// the V2 shim: RunPodSandbox → StopPodSandbox → RemovePodSandbox. This is the
25+
// minimum end-to-end smoke test that proves containerd → shim handshake works
26+
// on the V2 path.
27+
func Test_V2_LCOW_PodLifecycle(t *testing.T) {
28+
requireFeatures(t, featureLCOWV2)
29+
30+
pullRequiredLCOWImages(t, []string{imageLcowK8sPause})
31+
32+
client := newTestRuntimeClient(t)
33+
ctx, cancel := context.WithCancel(context.Background())
34+
defer cancel()
35+
36+
podReq := getRunPodSandboxRequest(t, lcowV2RuntimeHandler)
37+
podID := runPodSandbox(t, client, ctx, podReq)
38+
defer removePodSandbox(t, client, ctx, podID)
39+
defer stopPodSandbox(t, client, ctx, podID)
40+
}
41+
42+
// Test_V2_LCOW_ContainerLifecycle exercises a full container lifecycle inside
43+
// a V2 sandbox: RunPodSandbox → CreateContainer → StartContainer →
44+
// StopContainer → RemoveContainer → StopPodSandbox → RemovePodSandbox.
45+
func Test_V2_LCOW_ContainerLifecycle(t *testing.T) {
46+
requireFeatures(t, featureLCOWV2)
47+
48+
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, imageLcowAlpine})
49+
50+
client := newTestRuntimeClient(t)
51+
ctx, cancel := context.WithCancel(context.Background())
52+
defer cancel()
53+
54+
podReq := getRunPodSandboxRequest(t, lcowV2RuntimeHandler)
55+
podID := runPodSandbox(t, client, ctx, podReq)
56+
defer removePodSandbox(t, client, ctx, podID)
57+
defer stopPodSandbox(t, client, ctx, podID)
58+
59+
cReq := getCreateContainerRequest(podID, "alpine", imageLcowAlpine,
60+
[]string{"echo", "hello"}, podReq.Config)
61+
containerID := createContainer(t, client, ctx, cReq)
62+
defer removeContainer(t, client, ctx, containerID)
63+
64+
startContainer(t, client, ctx, containerID)
65+
stopContainer(t, client, ctx, containerID)
66+
}
67+
68+
// Test_V2_LCOW_ContainerExec runs a workload container and verifies that
69+
// ExecSync into it succeeds with the expected exit code. Validates the GCS
70+
// exec path through the V2 controller.
71+
func Test_V2_LCOW_ContainerExec(t *testing.T) {
72+
requireFeatures(t, featureLCOWV2)
73+
74+
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, imageLcowAlpine})
75+
76+
client := newTestRuntimeClient(t)
77+
ctx, cancel := context.WithCancel(context.Background())
78+
defer cancel()
79+
80+
podReq := getRunPodSandboxRequest(t, lcowV2RuntimeHandler)
81+
podID := runPodSandbox(t, client, ctx, podReq)
82+
defer removePodSandbox(t, client, ctx, podID)
83+
defer stopPodSandbox(t, client, ctx, podID)
84+
85+
cReq := getCreateContainerRequest(podID, "alpine", imageLcowAlpine,
86+
[]string{"top"}, podReq.Config)
87+
containerID := createContainer(t, client, ctx, cReq)
88+
defer removeContainer(t, client, ctx, containerID)
89+
90+
startContainer(t, client, ctx, containerID)
91+
defer stopContainer(t, client, ctx, containerID)
92+
93+
execResp, err := client.ExecSync(ctx, &runtime.ExecSyncRequest{
94+
ContainerId: containerID,
95+
Cmd: []string{"echo", "hello"},
96+
Timeout: 20,
97+
})
98+
if err != nil {
99+
t.Fatalf("ExecSync failed: %v", err)
100+
}
101+
if execResp.ExitCode != 0 {
102+
t.Fatalf("ExecSync returned exit code %d, stderr: %s", execResp.ExitCode, string(execResp.Stderr))
103+
}
104+
}

test/cri-containerd/main_test.go

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929

3030
// TODO: remove lcow when shim only tests are relocated
3131
lcowRuntimeHandler = "runhcs-lcow"
32+
lcowV2RuntimeHandler = "runhcs-lcow-v2"
3233
wcowProcessRuntimeHandler = "runhcs-wcow-process"
3334
wcowHypervisorRuntimeHandler = "runhcs-wcow-hypervisor"
3435
wcowHypervisor17763RuntimeHandler = "runhcs-wcow-hypervisor-17763"
@@ -98,6 +99,7 @@ var (
9899
// Make sure you update allFeatures below with any new features you add.
99100
const (
100101
featureLCOW = "LCOW"
102+
featureLCOWV2 = "LCOWV2"
101103
featureWCOWProcess = "WCOWProcess"
102104
featureWCOWHypervisor = "WCOWHypervisor"
103105
featureHostProcess = "HostProcess"
@@ -109,6 +111,7 @@ const (
109111

110112
var allFeatures = []string{
111113
featureLCOW,
114+
featureLCOWV2,
112115
featureWCOWProcess,
113116
featureWCOWHypervisor,
114117
featureHostProcess,
@@ -120,6 +123,15 @@ var allFeatures = []string{
120123

121124
func TestMain(m *testing.M) {
122125
flag.Parse()
126+
// LCOWV2 implies LCOW: the v2 shim IS an LCOW runtime, so a run gated
127+
// only on `-feature LCOWV2` should still execute tests that gate on
128+
// `featureLCOW`. Without this implication, the same test suite would
129+
// need parallel `-feature LCOW` and `-feature LCOWV2` invocations OR
130+
// every LCOW-gated test would have to be rewritten to accept either
131+
// flag. Mirrors the pattern established in the azcri test suite.
132+
if flagFeatures.IncludesExplicit() && flagFeatures.IsSet(featureLCOWV2) {
133+
flagFeatures.Include(featureLCOW)
134+
}
123135
os.Exit(m.Run())
124136
}
125137

@@ -205,10 +217,23 @@ func pullRequiredLCOWImages(tb testing.TB, images []string, opts ...SandboxConfi
205217
opts = append(opts, WithSandboxLabels(map[string]string{
206218
"sandbox-platform": "linux/amd64",
207219
}))
208-
pullRequiredImagesWithOptions(tb, images, opts...)
220+
// Set RuntimeHandler on ImageSpec so containerd CRI picks the LCOW
221+
// runtime's configured snapshotter (windows-lcow) and platform
222+
// (linux/amd64) rather than the default windows/amd64. The sandbox-
223+
// platform label alone is not honored by modern containerd (≥2.0).
224+
pullRequiredImagesWithRuntime(tb, images, lcowRuntimeHandlerForTest(tb), opts...)
209225
}
210226

211227
func pullRequiredImagesWithOptions(tb testing.TB, images []string, opts ...SandboxConfigOpt) {
228+
tb.Helper()
229+
pullRequiredImagesWithRuntime(tb, images, "", opts...)
230+
}
231+
232+
// pullRequiredImagesWithRuntime pulls each image with the given runtime
233+
// handler set on the CRI ImageSpec. Empty runtimeHandler means use the
234+
// containerd default. Tests pulling LCOW images should pass the LCOW handler
235+
// so containerd selects the windows-lcow snapshotter and linux/amd64 platform.
236+
func pullRequiredImagesWithRuntime(tb testing.TB, images []string, runtimeHandler string, opts ...SandboxConfigOpt) {
212237
tb.Helper()
213238
if len(images) < 1 {
214239
return
@@ -228,7 +253,8 @@ func pullRequiredImagesWithOptions(tb testing.TB, images []string, opts ...Sandb
228253
for _, image := range images {
229254
_, err := client.PullImage(ctx, &runtime.PullImageRequest{
230255
Image: &runtime.ImageSpec{
231-
Image: image,
256+
Image: image,
257+
RuntimeHandler: runtimeHandler,
232258
},
233259
SandboxConfig: sb,
234260
})

test/cri-containerd/unmap_vpmem_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const (
2828
// leave it intact and recycle, the bug no longer surfaces.
2929
func Test_Force_LayerUnmap_Not_In_Order(t *testing.T) {
3030
requireFeatures(t, featureLCOW)
31+
requireV1Only(t)
3132

3233
pullRequiredLCOWImages(t, []string{imageLcowK8sPause, ubuntu2004LargeLayers, ubuntu2204LargeLayers})
3334

test/functional/helpers_v2_test.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build windows && functional
2+
3+
package functional
4+
5+
import "testing"
6+
7+
// isLCOWV2 reports whether the LCOWV2 feature flag is set on the current test
8+
// invocation. Callers should prefer the higher-level helper requireV1Only so
9+
// the V2 selection logic stays in one place.
10+
func isLCOWV2() bool {
11+
return flagFeatures.IsSet(featureLCOWV2)
12+
}
13+
14+
// requireV1Only skips the test when the LCOWV2 feature flag is set. Use this
15+
// for tests that depend on V1-only features such as VPMEM, VHD/initrd boot
16+
// modes, KernelDirect, or other UVM knobs not exposed in the v2 builder.
17+
//
18+
// Mirrors the pattern established in the azcri repo and in the CRI test suite
19+
// in test/cri-containerd/.
20+
func requireV1Only(tb testing.TB) {
21+
tb.Helper()
22+
if isLCOWV2() {
23+
tb.Skip("test requires V1 shim features (VPMEM/VHD/initrd/KernelDirect) not exposed in V2")
24+
}
25+
}

0 commit comments

Comments
 (0)