diff --git a/cmd/containerd-shim-runhcs-v1/task_hcs.go b/cmd/containerd-shim-runhcs-v1/task_hcs.go index 9fbb1faf35..526bd9eea5 100644 --- a/cmd/containerd-shim-runhcs-v1/task_hcs.go +++ b/cmd/containerd-shim-runhcs-v1/task_hcs.go @@ -934,6 +934,20 @@ func (ht *hcsTask) updateWCOWContainerCPU(ctx context.Context, cpu *specs.Window if cpu.Shares != nil { req.Weight = int32(*cpu.Shares) } + if len(cpu.Affinity) > 0 { + // Validate CPU affinity. TODO: wire through to HCS once the + // correct Processor schema field is confirmed. + tempSpec := &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: cpu, + }, + }, + } + if _, err := hcsoci.ConvertCPUAffinity(tempSpec); err != nil { + return err + } + } return ht.requestUpdateContainer(ctx, resourcepaths.SiloProcessorResourcePath, req) } diff --git a/internal/hcsoci/hcsdoc_wcow.go b/internal/hcsoci/hcsdoc_wcow.go index d1d1a44c85..b2159283b4 100644 --- a/internal/hcsoci/hcsdoc_wcow.go +++ b/internal/hcsoci/hcsdoc_wcow.go @@ -31,6 +31,13 @@ import ( const createContainerSubdirectoryForProcessDumpSuffix = "{container_id}" +// Sentinel errors returned by ConvertCPUAffinity. +var ( + ErrCPUAffinityMultipleGroups = errors.New("cpu affinity with multiple processor groups is not supported") + ErrCPUAffinityNonZeroGroup = errors.New("cpu affinity processor group is not supported") + ErrCPUAffinityMaskZero = errors.New("cpu affinity mask must be non-zero") +) + // A simple wrapper struct around the container mount configs that should be added to the // container. type mountsConfig struct { @@ -94,6 +101,31 @@ func createMountsConfig(ctx context.Context, coi *createOptionsInternal) (*mount return &config, nil } +// ConvertCPUAffinity handles the logic of converting and validating the container's CPU affinity +// specified in the OCI spec to what HCS expects. +// +// Returns the CPU affinity bitmask (0 if not specified) and any validation error. +// Phase 2 limitations: +// - Multiple affinity entries are rejected +// - Non-zero processor groups are rejected +func ConvertCPUAffinity(spec *specs.Spec) (uint64, error) { + if spec.Windows == nil || spec.Windows.Resources == nil || spec.Windows.Resources.CPU == nil || len(spec.Windows.Resources.CPU.Affinity) == 0 { + return 0, nil + } + + affinity := spec.Windows.Resources.CPU.Affinity + if len(affinity) != 1 { + return 0, fmt.Errorf("%w: %d entries", ErrCPUAffinityMultipleGroups, len(affinity)) + } + if affinity[0].Group != 0 { + return 0, fmt.Errorf("%w: %d", ErrCPUAffinityNonZeroGroup, affinity[0].Group) + } + if affinity[0].Mask == 0 { + return 0, fmt.Errorf("%w", ErrCPUAffinityMaskZero) + } + return affinity[0].Mask, nil +} + // ConvertCPULimits handles the logic of converting and validating the containers CPU limits // specified in the OCI spec to what HCS expects. // @@ -184,6 +216,12 @@ func createWindowsContainerDocument(ctx context.Context, coi *createOptionsInter return nil, nil, err } + // Validate CPU affinity from the spec. TODO: wire through to HCS once the + // correct Processor schema field is confirmed. + if _, err := ConvertCPUAffinity(coi.Spec); err != nil { + return nil, nil, err + } + if coi.HostingSystem != nil && coi.ScaleCPULimitsToSandbox && cpuLimit > 0 { // When ScaleCPULimitsToSandbox is set and we are running in a UVM, we assume // the CPU limit has been calculated based on the number of processors on the diff --git a/internal/hcsoci/hcsdoc_wcow_test.go b/internal/hcsoci/hcsdoc_wcow_test.go new file mode 100644 index 0000000000..fdc5c2ec4b --- /dev/null +++ b/internal/hcsoci/hcsdoc_wcow_test.go @@ -0,0 +1,149 @@ +//go:build windows + +package hcsoci + +import ( + "errors" + "testing" + + specs "github.com/opencontainers/runtime-spec/specs-go" +) + +func TestConvertCPUAffinity_Group0MaskSet(t *testing.T) { + s := &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: &specs.WindowsCPUResources{ + Affinity: []specs.WindowsCPUGroupAffinity{ + {Mask: 0x3, Group: 0}, + }, + }, + }, + }, + } + + affinity, err := ConvertCPUAffinity(s) + if err != nil { + t.Fatalf("ConvertCPUAffinity failed: %v", err) + } + if affinity != 0x3 { + t.Fatalf("unexpected cpu affinity: got %d want %d", affinity, uint64(0x3)) + } +} + +func TestConvertCPUAffinity_MultiGroupRejected(t *testing.T) { + s := &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: &specs.WindowsCPUResources{ + Affinity: []specs.WindowsCPUGroupAffinity{ + {Mask: 0x1, Group: 0}, + {Mask: 0x1, Group: 1}, + }, + }, + }, + }, + } + + _, err := ConvertCPUAffinity(s) + if err == nil { + t.Fatal("expected error for multiple affinity entries") + } + if !errors.Is(err, ErrCPUAffinityMultipleGroups) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestConvertCPUAffinity_NonZeroGroupRejected(t *testing.T) { + s := &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: &specs.WindowsCPUResources{ + Affinity: []specs.WindowsCPUGroupAffinity{ + {Mask: 0x1, Group: 1}, + }, + }, + }, + }, + } + + _, err := ConvertCPUAffinity(s) + if err == nil { + t.Fatal("expected error for non-zero affinity group") + } + if !errors.Is(err, ErrCPUAffinityNonZeroGroup) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestConvertCPUAffinity_ZeroMaskRejected(t *testing.T) { + s := &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: &specs.WindowsCPUResources{ + Affinity: []specs.WindowsCPUGroupAffinity{ + {Mask: 0, Group: 0}, + }, + }, + }, + }, + } + + _, err := ConvertCPUAffinity(s) + if err == nil { + t.Fatal("expected error for zero affinity mask") + } + if !errors.Is(err, ErrCPUAffinityMaskZero) { + t.Fatalf("unexpected error: %v", err) + } +} + +func TestConvertCPUAffinity_NoAffinity(t *testing.T) { + testCases := []struct { + name string + spec *specs.Spec + }{ + { + name: "nil spec.Windows", + spec: &specs.Spec{}, + }, + { + name: "nil spec.Windows.Resources", + spec: &specs.Spec{ + Windows: &specs.Windows{}, + }, + }, + { + name: "nil spec.Windows.Resources.CPU", + spec: &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{}, + }, + }, + }, + { + name: "empty affinity slice", + spec: &specs.Spec{ + Windows: &specs.Windows{ + Resources: &specs.WindowsResources{ + CPU: &specs.WindowsCPUResources{ + Affinity: []specs.WindowsCPUGroupAffinity{}, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + affinity, err := ConvertCPUAffinity(tc.spec) + if err != nil { + t.Fatalf("ConvertCPUAffinity failed: %v", err) + } + if affinity != 0 { + t.Fatalf("expected zero affinity, got %d", affinity) + } + }) + } +}