Skip to content

Commit a48db21

Browse files
committed
[shimV2] adds core pod, container and process controllers
Adds the `internal/controller` package hierarchy with three new sub-packages that provide lifecycle management for Linux Containers on Windows (LCOW): - `linuxcontainer`: manages the full lifecycle of a single LCOW container inside a Utility VM, including host-side resource allocation (SCSI layers, Plan9 shares, vPCI devices), guest-side container creation via the GCS, and state machine transitions. - `pod`: manages a single pod running inside a UVM, owning the network controller and tracking all container controllers belonging to the pod. - `process`: manages individual process (exec) instances within a container, handling IO plumbing, signal delivery, exit status reporting, and a linear state machine. Each package includes comprehensive unit tests, mock types, and documentation. Signed-off-by: Harsh Rawat <harshrawat@microsoft.com>
1 parent c03ae0f commit a48db21

28 files changed

+7286
-0
lines changed

internal/controller/linuxcontainer/container.go

Lines changed: 599 additions & 0 deletions
Large diffs are not rendered by default.

internal/controller/linuxcontainer/container_test.go

Lines changed: 845 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//go:build windows && lcow
2+
3+
package linuxcontainer
4+
5+
import (
6+
"context"
7+
"fmt"
8+
9+
"github.com/Microsoft/hcsshim/internal/controller/device/vpci"
10+
"github.com/Microsoft/hcsshim/internal/log"
11+
"github.com/Microsoft/hcsshim/internal/logfields"
12+
13+
"github.com/opencontainers/runtime-spec/specs-go"
14+
"github.com/sirupsen/logrus"
15+
)
16+
17+
// allocateDevices reserves and maps vPCI devices for the container.
18+
func (c *Controller) allocateDevices(ctx context.Context, spec *specs.Spec) error {
19+
for idx := range spec.Windows.Devices {
20+
device := &spec.Windows.Devices[idx]
21+
22+
if !vpci.IsValidDeviceType(device.IDType) {
23+
return fmt.Errorf("reserve device %s: unsupported type %s", device.ID, device.IDType)
24+
}
25+
26+
// Parse the device path into a PCI ID and optional virtual function index.
27+
pciID, virtualFunctionIndex := vpci.GetDeviceInfoFromPath(device.ID)
28+
29+
// Reserve the device on the host.
30+
vmBusGUID, err := c.vpci.Reserve(ctx, vpci.Device{
31+
DeviceInstanceID: pciID,
32+
VirtualFunctionIndex: virtualFunctionIndex,
33+
})
34+
if err != nil {
35+
return fmt.Errorf("reserve device %s: %w", device.ID, err)
36+
}
37+
38+
// Map the device into the VM.
39+
if err = c.vpci.AddToVM(ctx, vmBusGUID); err != nil {
40+
return fmt.Errorf("add device %s to vm: %w", device.ID, err)
41+
}
42+
43+
log.G(ctx).WithFields(logrus.Fields{
44+
logfields.DeviceID: pciID,
45+
logfields.VFIndex: virtualFunctionIndex,
46+
logfields.VMBusGUID: vmBusGUID.String(),
47+
}).Trace("reserved and mapped vPCI device")
48+
49+
// Rewrite the spec entry so GCS references the VMBus GUID.
50+
device.ID = vmBusGUID.String()
51+
c.devices = append(c.devices, vmBusGUID)
52+
}
53+
54+
log.G(ctx).Debug("all vPCI devices allocated successfully")
55+
return nil
56+
}
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
//go:build windows && lcow
2+
3+
package linuxcontainer
4+
5+
import (
6+
"errors"
7+
"testing"
8+
9+
"github.com/Microsoft/hcsshim/internal/controller/device/vpci"
10+
"github.com/Microsoft/hcsshim/internal/controller/linuxcontainer/mocks"
11+
12+
"github.com/Microsoft/go-winio/pkg/guid"
13+
"github.com/opencontainers/runtime-spec/specs-go"
14+
"go.uber.org/mock/gomock"
15+
)
16+
17+
// newTestControllerAndSpec creates a Controller wired to a fresh vPCIController
18+
// mock alongside a minimal OCI spec populated with the provided Windows devices.
19+
func newTestControllerAndSpec(t *testing.T, devices ...specs.WindowsDevice) (*Controller, *specs.Spec, *mocks.MockvPCIController) {
20+
t.Helper()
21+
ctrl := gomock.NewController(t)
22+
vpciCtrl := mocks.NewMockvPCIController(ctrl)
23+
return &Controller{vpci: vpciCtrl}, &specs.Spec{
24+
Windows: &specs.Windows{
25+
Devices: devices,
26+
},
27+
}, vpciCtrl
28+
}
29+
30+
var (
31+
errReserve = errors.New("reserve failed")
32+
errAddToVM = errors.New("add to vm failed")
33+
)
34+
35+
// TestAllocateDevices_NoDevices verifies that allocateDevices succeeds without
36+
// any vPCI calls when the spec contains no Windows devices.
37+
func TestAllocateDevices_NoDevices(t *testing.T) {
38+
t.Parallel()
39+
c, spec, _ := newTestControllerAndSpec(t)
40+
41+
if err := c.allocateDevices(t.Context(), spec); err != nil {
42+
t.Fatalf("unexpected error: %v", err)
43+
}
44+
if len(c.devices) != 0 {
45+
t.Errorf("expected 0 tracked devices, got %d", len(c.devices))
46+
}
47+
}
48+
49+
// TestAllocateDevices_InvalidDeviceType verifies that allocateDevices returns an
50+
// error for unsupported device types, regardless of position in the device list.
51+
func TestAllocateDevices_InvalidDeviceType(t *testing.T) {
52+
t.Parallel()
53+
tests := []struct {
54+
name string
55+
devices []specs.WindowsDevice
56+
}{
57+
{
58+
name: "single-invalid",
59+
devices: []specs.WindowsDevice{
60+
{ID: "PCI\\VEN_1234&DEV_5678\\0", IDType: "unsupported-type"},
61+
},
62+
},
63+
{
64+
name: "invalid-before-valid",
65+
devices: []specs.WindowsDevice{
66+
{ID: "PCI\\VEN_AAAA&DEV_1111\\0", IDType: "bad-type"},
67+
{ID: "PCI\\VEN_BBBB&DEV_2222\\0", IDType: vpci.DeviceIDType},
68+
},
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
t.Parallel()
75+
c, spec, _ := newTestControllerAndSpec(t, tt.devices...)
76+
77+
// No Reserve or AddToVM calls expected.
78+
79+
if err := c.allocateDevices(t.Context(), spec); err == nil {
80+
t.Fatal("expected error for unsupported device type")
81+
}
82+
if len(c.devices) != 0 {
83+
t.Errorf("expected 0 tracked devices, got %d", len(c.devices))
84+
}
85+
})
86+
}
87+
}
88+
89+
// TestAllocateDevices_SingleDevice verifies the Reserve → AddToVM flow for each
90+
// supported device type, including VF index parsing and spec ID rewrite.
91+
func TestAllocateDevices_SingleDevice(t *testing.T) {
92+
t.Parallel()
93+
tests := []struct {
94+
name string
95+
deviceID string
96+
idType string
97+
expectPCI string
98+
expectVF uint16
99+
}{
100+
{
101+
name: "vpci-instance-id",
102+
deviceID: "PCI\\VEN_1234&DEV_5678\\0",
103+
idType: vpci.DeviceIDType,
104+
expectPCI: "PCI\\VEN_1234&DEV_5678\\0",
105+
expectVF: 0,
106+
},
107+
{
108+
name: "vpci-legacy-with-vf-index",
109+
deviceID: "PCI\\VEN_1234&DEV_5678\\0/3",
110+
idType: vpci.DeviceIDTypeLegacy,
111+
expectPCI: "PCI\\VEN_1234&DEV_5678\\0",
112+
expectVF: 3,
113+
},
114+
{
115+
name: "gpu",
116+
deviceID: "PCI\\VEN_ABCD&DEV_9876\\0",
117+
idType: vpci.GpuDeviceIDType,
118+
expectPCI: "PCI\\VEN_ABCD&DEV_9876\\0",
119+
expectVF: 0,
120+
},
121+
}
122+
123+
for _, tt := range tests {
124+
t.Run(tt.name, func(t *testing.T) {
125+
t.Parallel()
126+
c, spec, vpciCtrl := newTestControllerAndSpec(t, specs.WindowsDevice{
127+
ID: tt.deviceID,
128+
IDType: tt.idType,
129+
})
130+
131+
testGUID, _ := guid.NewV4()
132+
133+
vpciCtrl.EXPECT().
134+
Reserve(gomock.Any(), vpci.Device{
135+
DeviceInstanceID: tt.expectPCI,
136+
VirtualFunctionIndex: tt.expectVF,
137+
}).
138+
Return(testGUID, nil)
139+
vpciCtrl.EXPECT().
140+
AddToVM(gomock.Any(), testGUID).
141+
Return(nil)
142+
143+
if err := c.allocateDevices(t.Context(), spec); err != nil {
144+
t.Fatalf("unexpected error: %v", err)
145+
}
146+
147+
// Verify the spec entry was rewritten to the VMBus GUID.
148+
if got := spec.Windows.Devices[0].ID; got != testGUID.String() {
149+
t.Errorf("spec device ID = %q, want %q", got, testGUID.String())
150+
}
151+
152+
// Verify the GUID was tracked.
153+
if len(c.devices) != 1 || c.devices[0] != testGUID {
154+
t.Errorf("tracked devices = %v, want [%v]", c.devices, testGUID)
155+
}
156+
})
157+
}
158+
}
159+
160+
// TestAllocateDevices_SingleDeviceFailure verifies that Reserve and AddToVM
161+
// failures are propagated and no device is tracked.
162+
func TestAllocateDevices_SingleDeviceFailure(t *testing.T) {
163+
t.Parallel()
164+
tests := []struct {
165+
name string
166+
reserveErr error
167+
addToVMErr error
168+
wantWrapped error
169+
}{
170+
{
171+
name: "reserve-fails",
172+
reserveErr: errReserve,
173+
wantWrapped: errReserve,
174+
},
175+
{
176+
name: "add-to-vm-fails",
177+
addToVMErr: errAddToVM,
178+
wantWrapped: errAddToVM,
179+
},
180+
}
181+
182+
for _, tt := range tests {
183+
t.Run(tt.name, func(t *testing.T) {
184+
t.Parallel()
185+
c, spec, vpciCtrl := newTestControllerAndSpec(t, specs.WindowsDevice{
186+
ID: "PCI\\VEN_1234&DEV_5678\\0",
187+
IDType: vpci.DeviceIDType,
188+
})
189+
190+
testGUID, _ := guid.NewV4()
191+
vpciCtrl.EXPECT().
192+
Reserve(gomock.Any(), gomock.Any()).
193+
Return(testGUID, tt.reserveErr)
194+
195+
// AddToVM is only called when Reserve succeeds.
196+
if tt.reserveErr == nil {
197+
vpciCtrl.EXPECT().
198+
AddToVM(gomock.Any(), testGUID).
199+
Return(tt.addToVMErr)
200+
}
201+
202+
err := c.allocateDevices(t.Context(), spec)
203+
if err == nil {
204+
t.Fatal("expected error")
205+
}
206+
if !errors.Is(err, tt.wantWrapped) {
207+
t.Errorf("error = %v, want wrapping %v", err, tt.wantWrapped)
208+
}
209+
if len(c.devices) != 0 {
210+
t.Errorf("expected 0 tracked devices, got %d", len(c.devices))
211+
}
212+
})
213+
}
214+
}
215+
216+
// TestAllocateDevices_MultipleDevices verifies that allocateDevices correctly
217+
// handles multiple devices, reserving and adding each one independently.
218+
func TestAllocateDevices_MultipleDevices(t *testing.T) {
219+
t.Parallel()
220+
c, spec, vpciCtrl := newTestControllerAndSpec(t,
221+
specs.WindowsDevice{ID: "PCI\\VEN_AAAA&DEV_1111\\0", IDType: vpci.DeviceIDType},
222+
specs.WindowsDevice{ID: "PCI\\VEN_BBBB&DEV_2222\\0", IDType: vpci.GpuDeviceIDType},
223+
)
224+
225+
guidA, _ := guid.NewV4()
226+
guidB, _ := guid.NewV4()
227+
228+
vpciCtrl.EXPECT().
229+
Reserve(gomock.Any(), vpci.Device{
230+
DeviceInstanceID: "PCI\\VEN_AAAA&DEV_1111\\0",
231+
VirtualFunctionIndex: 0,
232+
}).
233+
Return(guidA, nil)
234+
vpciCtrl.EXPECT().
235+
AddToVM(gomock.Any(), guidA).
236+
Return(nil)
237+
238+
vpciCtrl.EXPECT().
239+
Reserve(gomock.Any(), vpci.Device{
240+
DeviceInstanceID: "PCI\\VEN_BBBB&DEV_2222\\0",
241+
VirtualFunctionIndex: 0,
242+
}).
243+
Return(guidB, nil)
244+
vpciCtrl.EXPECT().
245+
AddToVM(gomock.Any(), guidB).
246+
Return(nil)
247+
248+
if err := c.allocateDevices(t.Context(), spec); err != nil {
249+
t.Fatalf("unexpected error: %v", err)
250+
}
251+
252+
if len(c.devices) != 2 {
253+
t.Fatalf("expected 2 tracked devices, got %d", len(c.devices))
254+
}
255+
if c.devices[0] != guidA || c.devices[1] != guidB {
256+
t.Errorf("tracked GUIDs = %v, %v; want %v, %v", c.devices[0], c.devices[1], guidA, guidB)
257+
}
258+
if spec.Windows.Devices[0].ID != guidA.String() {
259+
t.Errorf("first device ID = %q, want %q", spec.Windows.Devices[0].ID, guidA.String())
260+
}
261+
if spec.Windows.Devices[1].ID != guidB.String() {
262+
t.Errorf("second device ID = %q, want %q", spec.Windows.Devices[1].ID, guidB.String())
263+
}
264+
}
265+
266+
// TestAllocateDevices_MultipleDevicesPartialFailure verifies that when the
267+
// second device fails (at Reserve or AddToVM), the first device is tracked
268+
// but the overall call returns the expected error.
269+
func TestAllocateDevices_MultipleDevicesPartialFailure(t *testing.T) {
270+
t.Parallel()
271+
tests := []struct {
272+
name string
273+
reserveErr error
274+
addToVMErr error
275+
wantWrapped error
276+
}{
277+
{
278+
name: "second-reserve-fails",
279+
reserveErr: errReserve,
280+
wantWrapped: errReserve,
281+
},
282+
{
283+
name: "second-add-to-vm-fails",
284+
addToVMErr: errAddToVM,
285+
wantWrapped: errAddToVM,
286+
},
287+
}
288+
289+
for _, tt := range tests {
290+
t.Run(tt.name, func(t *testing.T) {
291+
t.Parallel()
292+
c, spec, vpciCtrl := newTestControllerAndSpec(t,
293+
specs.WindowsDevice{ID: "PCI\\VEN_AAAA&DEV_1111\\0", IDType: vpci.DeviceIDType},
294+
specs.WindowsDevice{ID: "PCI\\VEN_BBBB&DEV_2222\\0", IDType: vpci.DeviceIDType},
295+
)
296+
297+
guidA, _ := guid.NewV4()
298+
guidB, _ := guid.NewV4()
299+
300+
// First device always succeeds.
301+
vpciCtrl.EXPECT().
302+
Reserve(gomock.Any(), vpci.Device{
303+
DeviceInstanceID: "PCI\\VEN_AAAA&DEV_1111\\0",
304+
VirtualFunctionIndex: 0,
305+
}).
306+
Return(guidA, nil)
307+
vpciCtrl.EXPECT().
308+
AddToVM(gomock.Any(), guidA).
309+
Return(nil)
310+
311+
// Second device fails at the configured step.
312+
vpciCtrl.EXPECT().
313+
Reserve(gomock.Any(), vpci.Device{
314+
DeviceInstanceID: "PCI\\VEN_BBBB&DEV_2222\\0",
315+
VirtualFunctionIndex: 0,
316+
}).
317+
Return(guidB, tt.reserveErr)
318+
319+
// AddToVM for the second device is only called when its Reserve succeeds.
320+
if tt.reserveErr == nil {
321+
vpciCtrl.EXPECT().
322+
AddToVM(gomock.Any(), guidB).
323+
Return(tt.addToVMErr)
324+
}
325+
326+
err := c.allocateDevices(t.Context(), spec)
327+
if err == nil {
328+
t.Fatal("expected error")
329+
}
330+
if !errors.Is(err, tt.wantWrapped) {
331+
t.Errorf("error = %v, want wrapping %v", err, tt.wantWrapped)
332+
}
333+
334+
// First device was already allocated before the second failed.
335+
if len(c.devices) != 1 {
336+
t.Errorf("expected 1 tracked device after partial failure, got %d", len(c.devices))
337+
}
338+
})
339+
}
340+
}

0 commit comments

Comments
 (0)