Skip to content
2 changes: 1 addition & 1 deletion internal/hcs/schema2/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* No description provided (generated by Swagger Codegen https://github.com/swagger-api/swagger-codegen)
*
* API version: 2.1
* API version: 2.4
* Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git)
*/

Expand Down
51 changes: 51 additions & 0 deletions internal/hcsoci/hcsdoc_wcow.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ import (

const createContainerSubdirectoryForProcessDumpSuffix = "{container_id}"

// Sentinel errors returned by ConvertCPUAffinity.
var (
// ErrCPUAffinityMultipleGroupsNotSupported is returned when multiple processor-group
// affinity entries are requested on a host older than Windows Server 2022 (build 20348),
// which does not support multi-group affinity for job object silos.
// On Windows Server 2022+, multiple processor groups are fully supported.
ErrCPUAffinityMultipleGroupsNotSupported = errors.New("cpu affinity with multiple processor groups requires Windows Server 2022 or later")
// ErrCPUAffinityNonZeroGroupNotSupported is returned when a non-zero processor group is
// requested on a host older than Windows Server 2022 (build 20348).
// On Windows Server 2022+, non-zero processor groups are fully supported.
ErrCPUAffinityNonZeroGroupNotSupported = errors.New("cpu affinity with a non-zero processor group requires Windows Server 2022 or later")
// ErrCPUAffinityMaskZero is returned when an affinity entry has a zero bitmask,
// which would select no processors and is always invalid.
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 {
Expand Down Expand Up @@ -94,6 +110,41 @@ func createMountsConfig(ctx context.Context, coi *createOptionsInternal) (*mount
return &config, nil
}

// ValidateCPUAffinity handles the logic of validating the container's CPU affinity
// specified in the OCI spec.
//
// Returns the validated affinity entries (nil if not specified) and any validation error.
// Multiple processor groups and non-zero group numbers require Windows Server 2022
// (build 20348) or later; on older hosts only a single entry for group 0 is accepted.
func ValidateCPUAffinity(spec *specs.Spec) ([]specs.WindowsCPUGroupAffinity, error) {
if spec.Windows == nil || spec.Windows.Resources == nil || spec.Windows.Resources.CPU == nil || len(spec.Windows.Resources.CPU.Affinity) == 0 {
return nil, nil
}

affinity := spec.Windows.Resources.CPU.Affinity

// Zero masks are never valid regardless of OS version.
for i, a := range affinity {
if a.Mask == 0 {
return nil, fmt.Errorf("%w: entry %d has zero mask", ErrCPUAffinityMaskZero, i)
}
}

// Determine whether multi-group features are needed: either multiple entries,
// or a single entry targeting a non-zero processor group.
multiGroup := len(affinity) > 1 || affinity[0].Group != 0

// Multiple processor groups are only supported on Windows Server 2022+.
if multiGroup && osversion.Build() < osversion.LTSC2022 {
if len(affinity) > 1 {
return nil, fmt.Errorf("%w: %d entries", ErrCPUAffinityMultipleGroupsNotSupported, len(affinity))
}
return nil, fmt.Errorf("%w: group %d", ErrCPUAffinityNonZeroGroupNotSupported, affinity[0].Group)
}

return affinity, nil
}

// ConvertCPULimits handles the logic of converting and validating the containers CPU limits
// specified in the OCI spec to what HCS expects.
//
Expand Down
171 changes: 171 additions & 0 deletions internal/hcsoci/hcsdoc_wcow_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//go:build windows

package hcsoci

import (
"errors"
"testing"

specs "github.com/opencontainers/runtime-spec/specs-go"

"github.com/Microsoft/hcsshim/osversion"
)

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},
},
},
},
},
}

affinities, err := ConvertCPUAffinity(s)
if err != nil {
t.Fatalf("ConvertCPUAffinity failed: %v", err)
}
if len(affinities) != 1 || affinities[0].Mask != 0x3 || affinities[0].Group != 0 {
t.Fatalf("unexpected cpu affinity: got %v", affinities)
}
}

func TestConvertCPUAffinity_MultiGroup(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},
},
},
},
},
}

affinities, err := ConvertCPUAffinity(s)
if osversion.Build() >= osversion.LTSC2022 {
// Multi-group is supported on WS2022+.
if err != nil {
t.Fatalf("expected success for multi-group on WS2022+, got: %v", err)
}
if len(affinities) != 2 {
t.Fatalf("expected 2 affinity entries, got %d", len(affinities))
}
} else {
if err == nil {
t.Fatal("expected error for multiple affinity entries on pre-WS2022")
}
if !errors.Is(err, ErrCPUAffinityMultipleGroupsNotSupported) {
t.Fatalf("unexpected error: %v", err)
}
}
}

func TestConvertCPUAffinity_NonZeroGroup(t *testing.T) {
s := &specs.Spec{
Windows: &specs.Windows{
Resources: &specs.WindowsResources{
CPU: &specs.WindowsCPUResources{
Affinity: []specs.WindowsCPUGroupAffinity{
{Mask: 0x1, Group: 1},
},
},
},
},
}

affinities, err := ConvertCPUAffinity(s)
if osversion.Build() >= osversion.LTSC2022 {
// Non-zero group is supported on WS2022+.
if err != nil {
t.Fatalf("expected success for non-zero group on WS2022+, got: %v", err)
}
if len(affinities) != 1 || affinities[0].Group != 1 {
t.Fatalf("unexpected affinity: got %v", affinities)
}
} else {
if err == nil {
t.Fatal("expected error for non-zero affinity group on pre-WS2022")
}
if !errors.Is(err, ErrCPUAffinityNonZeroGroupNotSupported) {
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) {
affinities, err := ConvertCPUAffinity(tc.spec)
if err != nil {
t.Fatalf("ConvertCPUAffinity failed: %v", err)
}
if len(affinities) != 0 {
t.Fatalf("expected empty affinities, got %v", affinities)
}
})
}
}
29 changes: 15 additions & 14 deletions internal/jobcontainers/oci.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package jobcontainers

import (
"context"
"fmt"

"github.com/Microsoft/hcsshim/internal/hcsoci"
"github.com/Microsoft/hcsshim/internal/jobobject"
Expand Down Expand Up @@ -41,19 +40,21 @@ func specToLimits(ctx context.Context, cid string, s *specs.Spec) (*jobobject.Jo
return nil, err
}

var cpuAffinity uint64
if s.Windows != nil && s.Windows.Resources != nil && s.Windows.Resources.CPU != nil && len(s.Windows.Resources.CPU.Affinity) > 0 {
affinity := s.Windows.Resources.CPU.Affinity
if len(affinity) != 1 {
return nil, fmt.Errorf("cpu affinity with multiple processor groups is not supported")
}
if affinity[0].Group != 0 {
return nil, fmt.Errorf("cpu affinity processor group %d is not supported", affinity[0].Group)
}
if affinity[0].Mask == 0 {
return nil, fmt.Errorf("cpu affinity mask must be non-zero")
// Validate and retrieve CPU affinity using the shared helper, which enforces the
// OS version gate for multi-group support (WS2022+).
affinities, err := hcsoci.ConvertCPUAffinity(s)
if err != nil {
return nil, err
}
var groupAffinities []jobobject.GroupAffinity
if len(affinities) > 0 {
groupAffinities = make([]jobobject.GroupAffinity, len(affinities))
for i, a := range affinities {
groupAffinities[i] = jobobject.GroupAffinity{
Mask: a.Mask,
Group: uint16(a.Group),
}
}
cpuAffinity = affinity[0].Mask
}

realCPULimit, realCPUWeight := uint32(cpuLimit), uint32(cpuWeight)
Expand All @@ -77,7 +78,7 @@ func specToLimits(ctx context.Context, cid string, s *specs.Spec) (*jobobject.Jo
return &jobobject.JobLimits{
CPULimit: realCPULimit,
CPUWeight: realCPUWeight,
CPUAffinity: cpuAffinity,
GroupAffinities: groupAffinities,
MaxIOPS: maxIops,
MaxBandwidth: maxBandwidth,
MemoryLimitInBytes: memLimitMB * memory.MiB,
Expand Down
Loading
Loading