From aa44b8cd18650470e6132398f002f35ff2ad3745 Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Fri, 10 Jul 2026 16:35:48 +0200 Subject: [PATCH 1/4] fix(e2e): restrict VM SKU fallbacks to RP-allowlisted instance types The e2e VM-size selectors picked SKUs based only on Azure availability and capabilities, with no awareness of the ARO-HCP RP instance-type allowlist (cluster-service cloud-resource-constraints-config). When a region/subscription made the preferred candidates unavailable, selection could fall through to a SKU that Azure advertises but the RP rejects, failing node pool creation with: RESPONSE 400 InvalidRequestContent: Machine type 'Standard_D8lds_v6' is not supported. Only the plain "s" and AMD "as" D-series variants are enabled in the RP allowlist; the local-disk "ds"/"lds"/"ads" variants are not. This restricts the Preferred lists and NamePattern fallback regexes of the worker selectors to allowlisted families: - default-worker: D8ds_v5/D8lds_v6 -> D8s_v5/D8s_v6 (D8as_v5 kept) - small-worker: D4ds_v5/D4lds_v6 -> D4s_v5/D4s_v6 (D4as_v5 kept) - ephemeral-osdisk-worker: Dsv3 is the only allowlisted family with cache-based ephemeral OS disk support, so candidates are the Dsv3 family (D8s_v3 preferred, D16s_v3/D32s_v3 as availability fallback) The NamePattern regexes are tightened from `[^p]*s_v[3456]` to `(?:a)?s_v[3456]` so the deterministic fallback path also cannot pick a non-allowlisted local-disk SKU. Jumpbox is unchanged (plain Azure VM, not subject to RP instance-type constraints). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/util/framework/vm_sku_selection.go | 45 ++++++++++++++++--------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/test/util/framework/vm_sku_selection.go b/test/util/framework/vm_sku_selection.go index 7cd0db64ccc..47f25841d76 100644 --- a/test/util/framework/vm_sku_selection.go +++ b/test/util/framework/vm_sku_selection.go @@ -407,15 +407,23 @@ func skuCapabilityInt(sku *armcompute.ResourceSKU, name string) (int, bool) { // default node pool. Standard_D8s_v3 is preferred to preserve historical // behaviour; the fallback keeps the D-series general-purpose, >=8 vCPU shape. // +// Candidates are restricted to SKU families that are enabled in the ARO-HCP RP +// instance-type allowlist (see cluster-service +// cloud-resource-constraints-config: only the plain "s" and AMD "as" D-series +// variants are enabled; the "ds"/"lds"/"ads" local-disk variants are NOT). A +// SKU that Azure advertises but the RP rejects would otherwise be selected as a +// fallback and fail node pool creation with InvalidRequestContent. +// // The fallback pattern is capped at D8 (8 vCPUs) to avoid selecting large SKUs // (D32, D64, D96) that are slow to provision and easily exhaust subscription -// quota in test environments. The [^p] exclusion prevents matching Arm64 "p" -// variants (e.g. Standard_D8ps_v6). +// quota in test environments. The (?:a)? in the pattern matches only the plain +// "s" and "as" variants and excludes the non-allowlisted local-disk ("ds", +// "lds", "ads") and Arm64 "p" (e.g. Standard_D8ps_v6) variants. func DefaultWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ Name: "default-worker", - Preferred: []string{DefaultWorkerVMSize, "Standard_D8ds_v5", "Standard_D8as_v5", "Standard_D8lds_v6"}, - NamePattern: regexp.MustCompile(`^Standard_D[4-8][^p]*s_v[3456]$`), + Preferred: []string{DefaultWorkerVMSize, "Standard_D8s_v5", "Standard_D8as_v5", "Standard_D8s_v6"}, + NamePattern: regexp.MustCompile(`^Standard_D[4-8](?:a)?s_v[3456]$`), MinVCPUs: 8, } } @@ -423,13 +431,14 @@ func DefaultWorkerVMSizeSelector() VMSizeSelector { // SmallWorkerVMSizeSelector selects a smaller general-purpose worker SKU used by // tests that want faster provisioning. // -// The fallback pattern is capped at D4 to keep provisioning fast and quota use -// low. The [^p] exclusion prevents matching Arm64 variants. +// Candidates are restricted to RP-allowlisted D-series families (plain "s" and +// AMD "as" only); see DefaultWorkerVMSizeSelector for the rationale. The +// fallback pattern is capped at D4 to keep provisioning fast and quota use low. func SmallWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ Name: "small-worker", - Preferred: []string{SmallWorkerVMSize, "Standard_D4ds_v5", "Standard_D4as_v5", "Standard_D4lds_v6"}, - NamePattern: regexp.MustCompile(`^Standard_D[2-4][^p]*s_v[3456]$`), + Preferred: []string{SmallWorkerVMSize, "Standard_D4s_v5", "Standard_D4as_v5", "Standard_D4s_v6"}, + NamePattern: regexp.MustCompile(`^Standard_D[2-4](?:a)?s_v[3456]$`), MinVCPUs: 4, } } @@ -449,18 +458,22 @@ func JumpboxVMSizeSelector() VMSizeSelector { } // EphemeralOSDiskWorkerVMSizeSelector selects a general-purpose worker SKU that -// supports ephemeral OS disks (requires local/cache storage). Standard_D8s_v3 -// is preferred because its cache disk supports ephemeral placement; the dd -// variants (Ddsv5) and lds variants (Dldsv6, NVMe placement) are fallbacks. +// supports ephemeral OS disks (requires local/cache storage) and is enabled in +// the ARO-HCP RP instance-type allowlist. // -// SKUs without local storage (e.g. Dsv5) are automatically excluded via the -// RequireEphemeralOSDisk constraint. The [^p] exclusion prevents matching Arm64 -// variants. +// Among the RP-allowlisted non-ARM D-series families, only the v3 "s" series +// (Dsv3) supports ephemeral OS disks via its cache disk. The allowlisted v4/v5/ +// v6 "s" variants have no local storage, and the local-disk "ds"/"lds" variants +// (which do support ephemeral placement) are NOT in the allowlist. Selecting +// one of those would fail node pool creation with InvalidRequestContent, so the +// candidates are restricted to the Dsv3 family (larger sizes act as an +// availability fallback). SKUs without local storage are additionally excluded +// via the RequireEphemeralOSDisk constraint. func EphemeralOSDiskWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ Name: "ephemeral-osdisk-worker", - Preferred: []string{DefaultWorkerVMSize, "Standard_D8ds_v5", "Standard_D8ads_v5", "Standard_D8lds_v6"}, - NamePattern: regexp.MustCompile(`^Standard_D[4-8][^p]*s_v[3456]$`), + Preferred: []string{DefaultWorkerVMSize, "Standard_D16s_v3", "Standard_D32s_v3"}, + NamePattern: regexp.MustCompile(`^Standard_D(?:8|16|32)s_v3$`), MinVCPUs: 8, RequireEphemeralOSDisk: true, } From 692ab106ef9b68267833f91b41c2f67763650a4c Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 14 Jul 2026 17:30:50 +0200 Subject: [PATCH 2/4] fix(e2e): tighten SKU fallback regex to allowlisted versions + tests Address review feedback on the VM SKU selection fallback patterns: - The fallback NamePattern `(?:a)?s_v[3456]` matched AMD "as" SKUs for all of v3-v6, but the RP allowlist only enables D*as for v4/v5 (D*as_v3 and D*as_v6 are not allowlisted). If the preferred candidates were unusable, the deterministic fallback could still select a non-allowlisted "as" SKU and hit the same InvalidRequestContent failure. Split the alternation into `(s_v[3456]|as_v[45])` so only allowlisted family/version combinations match: plain "s" for v3-v6, AMD "as" for v4/v5 only. - Add regression tests asserting the production worker selector patterns reject disallowed variants (ds/lds/ads/p, as_v3, as_v6) and accept the allowlisted ones, plus end-to-end selectVMSize tests proving the fallback never returns a non-allowlisted SKU (reproducing the prod uksouth D8lds_v6 failure) and prefers an allowlisted SKU when one is available. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/util/framework/vm_sku_selection.go | 20 ++-- test/util/framework/vm_sku_selection_test.go | 97 ++++++++++++++++++++ 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/test/util/framework/vm_sku_selection.go b/test/util/framework/vm_sku_selection.go index 47f25841d76..2aee30b2b02 100644 --- a/test/util/framework/vm_sku_selection.go +++ b/test/util/framework/vm_sku_selection.go @@ -416,14 +416,16 @@ func skuCapabilityInt(sku *armcompute.ResourceSKU, name string) (int, bool) { // // The fallback pattern is capped at D8 (8 vCPUs) to avoid selecting large SKUs // (D32, D64, D96) that are slow to provision and easily exhaust subscription -// quota in test environments. The (?:a)? in the pattern matches only the plain -// "s" and "as" variants and excludes the non-allowlisted local-disk ("ds", -// "lds", "ads") and Arm64 "p" (e.g. Standard_D8ps_v6) variants. +// quota in test environments. The alternation matches only the RP-allowlisted +// versions of each family: plain "s" is enabled for v3-v6, but AMD "as" is +// enabled only for v4/v5 (as_v3 and as_v6 are NOT allowlisted). The pattern +// also excludes the non-allowlisted local-disk ("ds", "lds", "ads") and Arm64 +// "p" (e.g. Standard_D8ps_v6) variants. func DefaultWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ Name: "default-worker", Preferred: []string{DefaultWorkerVMSize, "Standard_D8s_v5", "Standard_D8as_v5", "Standard_D8s_v6"}, - NamePattern: regexp.MustCompile(`^Standard_D[4-8](?:a)?s_v[3456]$`), + NamePattern: regexp.MustCompile(`^Standard_D[4-8](s_v[3456]|as_v[45])$`), MinVCPUs: 8, } } @@ -431,14 +433,16 @@ func DefaultWorkerVMSizeSelector() VMSizeSelector { // SmallWorkerVMSizeSelector selects a smaller general-purpose worker SKU used by // tests that want faster provisioning. // -// Candidates are restricted to RP-allowlisted D-series families (plain "s" and -// AMD "as" only); see DefaultWorkerVMSizeSelector for the rationale. The -// fallback pattern is capped at D4 to keep provisioning fast and quota use low. +// Candidates are restricted to RP-allowlisted D-series families (plain "s" for +// v3-v6 and AMD "as" for v4/v5 only); see DefaultWorkerVMSizeSelector for the +// rationale. The fallback pattern is capped at D4 to keep provisioning fast and +// quota use low. MinVCPUs=4 additionally excludes the 2-vCPU (D2) sizes the +// [2-4] range would otherwise admit. func SmallWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ Name: "small-worker", Preferred: []string{SmallWorkerVMSize, "Standard_D4s_v5", "Standard_D4as_v5", "Standard_D4s_v6"}, - NamePattern: regexp.MustCompile(`^Standard_D[2-4](?:a)?s_v[3456]$`), + NamePattern: regexp.MustCompile(`^Standard_D[2-4](s_v[3456]|as_v[45])$`), MinVCPUs: 4, } } diff --git a/test/util/framework/vm_sku_selection_test.go b/test/util/framework/vm_sku_selection_test.go index 9a70017ea89..a7893a6a1ab 100644 --- a/test/util/framework/vm_sku_selection_test.go +++ b/test/util/framework/vm_sku_selection_test.go @@ -315,3 +315,100 @@ func TestSelectVMSize(t *testing.T) { }) } } + +// productionWorkerSelectors are the selectors whose fallback NamePattern is a +// correctness boundary: their deterministic fallback must never select a SKU +// outside the ARO-HCP RP instance-type allowlist (cluster-service +// cloud-resource-constraints-config), or node pool creation fails with +// InvalidRequestContent. +func productionWorkerSelectors() []VMSizeSelector { + return []VMSizeSelector{ + DefaultWorkerVMSizeSelector(), + SmallWorkerVMSizeSelector(), + EphemeralOSDiskWorkerVMSizeSelector(), + } +} + +// TestWorkerSelectorPatternsRejectNonAllowlistedSKUs guards against future +// widening of the fallback regexes. Every SKU below is advertised by Azure but +// NOT in the RP allowlist; the fallback NamePattern must reject all of them. +func TestWorkerSelectorPatternsRejectNonAllowlistedSKUs(t *testing.T) { + disallowed := []string{ + // Local-disk variants (ds/lds/ads) are not allowlisted. + "Standard_D8ds_v5", "Standard_D4ds_v5", + "Standard_D8lds_v6", "Standard_D4lds_v6", "Standard_D2lds_v6", + "Standard_D8ads_v5", "Standard_D4ads_v5", + // AMD "as" is allowlisted only for v4/v5, not v3 or v6. + "Standard_D8as_v3", "Standard_D8as_v6", + "Standard_D4as_v3", "Standard_D4as_v6", + // Arm64 "p" variants are not allowlisted for these selectors. + "Standard_D8ps_v6", "Standard_D8plds_v6", + } + for _, sel := range productionWorkerSelectors() { + if sel.NamePattern == nil { + t.Fatalf("selector %q has no NamePattern; it is required as the allowlist boundary", sel.Name) + } + for _, name := range disallowed { + if sel.NamePattern.MatchString(name) { + t.Errorf("selector %q NamePattern must not match non-allowlisted SKU %q", sel.Name, name) + } + } + } +} + +// TestWorkerSelectorPatternsAcceptAllowlistedSKUs ensures the patterns still +// admit the RP-allowlisted SKUs each selector is expected to fall back to. +func TestWorkerSelectorPatternsAcceptAllowlistedSKUs(t *testing.T) { + cases := map[string][]string{ + "default-worker": {"Standard_D8s_v3", "Standard_D8s_v4", "Standard_D8s_v5", "Standard_D8s_v6", "Standard_D8as_v4", "Standard_D8as_v5"}, + "small-worker": {"Standard_D4s_v3", "Standard_D4s_v4", "Standard_D4s_v5", "Standard_D4s_v6", "Standard_D4as_v4", "Standard_D4as_v5"}, + "ephemeral-osdisk-worker": {"Standard_D8s_v3", "Standard_D16s_v3", "Standard_D32s_v3"}, + } + byName := map[string]VMSizeSelector{} + for _, sel := range productionWorkerSelectors() { + byName[sel.Name] = sel + } + for name, skus := range cases { + sel, ok := byName[name] + if !ok { + t.Fatalf("no production selector named %q", name) + } + for _, sku := range skus { + if !sel.NamePattern.MatchString(sku) { + t.Errorf("selector %q NamePattern must match allowlisted SKU %q", name, sku) + } + } + } +} + +// TestSelectVMSizeNeverPicksNonAllowlistedFallback reproduces the prod uksouth +// failure: the preferred SKUs are unusable and only a non-allowlisted SKU +// (Standard_D8lds_v6) is otherwise available. Selection must return +// ErrNoUsableVMSize rather than the non-allowlisted SKU, which the RP rejects +// with InvalidRequestContent. +func TestSelectVMSizeNeverPicksNonAllowlistedFallback(t *testing.T) { + skus := []*armcompute.ResourceSKU{ + makeSKU("Standard_D8lds_v6", testLocation, withCapability(capabilityVCPUs, "8")), + } + _, _, err := selectVMSize(skus, testLocation, DefaultWorkerVMSizeSelector()) + if !errors.Is(err, ErrNoUsableVMSize) { + t.Fatalf("expected ErrNoUsableVMSize when only a non-allowlisted SKU is available, got err=%v", err) + } +} + +// TestSelectVMSizeFallbackPrefersAllowlisted verifies that when both a +// non-allowlisted and an allowlisted (but non-preferred) SKU are available, the +// deterministic fallback selects the allowlisted one. +func TestSelectVMSizeFallbackPrefersAllowlisted(t *testing.T) { + skus := []*armcompute.ResourceSKU{ + makeSKU("Standard_D8lds_v6", testLocation, withCapability(capabilityVCPUs, "8")), // non-allowlisted, usable + makeSKU("Standard_D8s_v4", testLocation, withCapability(capabilityVCPUs, "8")), // allowlisted, not in Preferred + } + got, _, err := selectVMSize(skus, testLocation, DefaultWorkerVMSizeSelector()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "Standard_D8s_v4" { + t.Fatalf("expected allowlisted fallback Standard_D8s_v4, got %q", got) + } +} From 5af24c87ec9ea6caed484715e547cde264c0183f Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 14 Jul 2026 17:44:59 +0200 Subject: [PATCH 3/4] fix(e2e): use diverse-family 8-vCPU fallbacks for ephemeral OS disk worker The ephemeral OS disk worker selector previously preferred larger sizes of the same family (Standard_D16s_v3, Standard_D32s_v3) as fallbacks. That is both oversized (worker SKUs should cap at 8 vCPUs) and ineffective: SKU restrictions typically apply to an entire family in a region, so a larger size of an already-restricted family is no more likely to be usable. Replace the fallbacks with 8-vCPU, RP-allowlisted, ephemeral-capable SKUs drawn from distinct families (Intel/AMD, D/E series): Standard_D8s_v3, D8as_v4, E8s_v3, E8as_v4. Cap the fallback NamePattern at size 8 accordingly. Add regression tests asserting the worker patterns reject >8-vCPU SKUs and that the ephemeral selector caps at 8 vCPUs and prefers a different family over a larger same-family size. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/util/framework/vm_sku_selection.go | 28 ++++++---- test/util/framework/vm_sku_selection_test.go | 55 +++++++++++++++++++- 2 files changed, 71 insertions(+), 12 deletions(-) diff --git a/test/util/framework/vm_sku_selection.go b/test/util/framework/vm_sku_selection.go index 2aee30b2b02..c415f420710 100644 --- a/test/util/framework/vm_sku_selection.go +++ b/test/util/framework/vm_sku_selection.go @@ -465,19 +465,25 @@ func JumpboxVMSizeSelector() VMSizeSelector { // supports ephemeral OS disks (requires local/cache storage) and is enabled in // the ARO-HCP RP instance-type allowlist. // -// Among the RP-allowlisted non-ARM D-series families, only the v3 "s" series -// (Dsv3) supports ephemeral OS disks via its cache disk. The allowlisted v4/v5/ -// v6 "s" variants have no local storage, and the local-disk "ds"/"lds" variants -// (which do support ephemeral placement) are NOT in the allowlist. Selecting -// one of those would fail node pool creation with InvalidRequestContent, so the -// candidates are restricted to the Dsv3 family (larger sizes act as an -// availability fallback). SKUs without local storage are additionally excluded -// via the RequireEphemeralOSDisk constraint. +// Ephemeral OS disk support depends on the SKU generation, not just the family: +// among the RP-allowlisted x86 families, only a subset of 8-vCPU SKUs advertise +// EphemeralOSDiskSupported=True. The fallbacks are drawn from distinct families +// (Intel/AMD, D/E series) rather than larger sizes of the same family, because +// SKU restrictions typically apply to an entire family in a region, so a +// larger size of an already-restricted family is no more likely to be usable. +// All candidates are capped at 8 vCPUs to keep provisioning fast and quota use +// low. SKUs without local storage are additionally excluded via the +// RequireEphemeralOSDisk constraint. func EphemeralOSDiskWorkerVMSizeSelector() VMSizeSelector { return VMSizeSelector{ - Name: "ephemeral-osdisk-worker", - Preferred: []string{DefaultWorkerVMSize, "Standard_D16s_v3", "Standard_D32s_v3"}, - NamePattern: regexp.MustCompile(`^Standard_D(?:8|16|32)s_v3$`), + Name: "ephemeral-osdisk-worker", + Preferred: []string{ + DefaultWorkerVMSize, // Standard_D8s_v3 - Intel D-series v3 + "Standard_D8as_v4", // AMD D-series v4 + "Standard_E8s_v3", // Intel E-series v3 + "Standard_E8as_v4", // AMD E-series v4 + }, + NamePattern: regexp.MustCompile(`^Standard_[DE]8(s_v3|as_v4)$`), MinVCPUs: 8, RequireEphemeralOSDisk: true, } diff --git a/test/util/framework/vm_sku_selection_test.go b/test/util/framework/vm_sku_selection_test.go index a7893a6a1ab..58715a6d169 100644 --- a/test/util/framework/vm_sku_selection_test.go +++ b/test/util/framework/vm_sku_selection_test.go @@ -354,6 +354,20 @@ func TestWorkerSelectorPatternsRejectNonAllowlistedSKUs(t *testing.T) { } } } + + // These are RP-allowlisted but exceed the 8-vCPU cap enforced by the worker + // selectors; their fallback patterns must reject them regardless. + tooBig := []string{ + "Standard_D16s_v3", "Standard_D32s_v3", "Standard_D64s_v3", + "Standard_D16as_v4", "Standard_E16s_v3", "Standard_E16as_v4", + } + for _, sel := range productionWorkerSelectors() { + for _, name := range tooBig { + if sel.NamePattern.MatchString(name) { + t.Errorf("selector %q NamePattern must not match >8-vCPU SKU %q (worker selectors cap at 8 vCPUs)", sel.Name, name) + } + } + } } // TestWorkerSelectorPatternsAcceptAllowlistedSKUs ensures the patterns still @@ -362,7 +376,7 @@ func TestWorkerSelectorPatternsAcceptAllowlistedSKUs(t *testing.T) { cases := map[string][]string{ "default-worker": {"Standard_D8s_v3", "Standard_D8s_v4", "Standard_D8s_v5", "Standard_D8s_v6", "Standard_D8as_v4", "Standard_D8as_v5"}, "small-worker": {"Standard_D4s_v3", "Standard_D4s_v4", "Standard_D4s_v5", "Standard_D4s_v6", "Standard_D4as_v4", "Standard_D4as_v5"}, - "ephemeral-osdisk-worker": {"Standard_D8s_v3", "Standard_D16s_v3", "Standard_D32s_v3"}, + "ephemeral-osdisk-worker": {"Standard_D8s_v3", "Standard_D8as_v4", "Standard_E8s_v3", "Standard_E8as_v4"}, } byName := map[string]VMSizeSelector{} for _, sel := range productionWorkerSelectors() { @@ -412,3 +426,42 @@ func TestSelectVMSizeFallbackPrefersAllowlisted(t *testing.T) { t.Fatalf("expected allowlisted fallback Standard_D8s_v4, got %q", got) } } + +// TestEphemeralSelectorCapsAtEightVCPUs verifies the ephemeral OS disk worker +// selector never selects a SKU larger than 8 vCPUs, even when a larger +// ephemeral-capable, allowlisted SKU is the only one available. +func TestEphemeralSelectorCapsAtEightVCPUs(t *testing.T) { + skus := []*armcompute.ResourceSKU{ + makeSKU("Standard_D16s_v3", testLocation, + withCapability(capabilityVCPUs, "16"), + withCapability(capabilityEphemeralOSDiskSupported, "True")), + } + _, _, err := selectVMSize(skus, testLocation, EphemeralOSDiskWorkerVMSizeSelector()) + if !errors.Is(err, ErrNoUsableVMSize) { + t.Fatalf("expected ErrNoUsableVMSize (>8-vCPU SKU must be rejected), got err=%v", err) + } +} + +// TestEphemeralSelectorSelectsDifferentFamily verifies that when the Intel +// D-series preferred SKUs are unusable, the selector falls through to an +// ephemeral-capable, allowlisted SKU from a different family rather than a +// larger size of the same family. +func TestEphemeralSelectorSelectsDifferentFamily(t *testing.T) { + skus := []*armcompute.ResourceSKU{ + // Same-family larger sizes are available but must not be chosen. + makeSKU("Standard_D16s_v3", testLocation, + withCapability(capabilityVCPUs, "16"), + withCapability(capabilityEphemeralOSDiskSupported, "True")), + // A different-family 8-vCPU ephemeral SKU that should be selected. + makeSKU("Standard_E8as_v4", testLocation, + withCapability(capabilityVCPUs, "8"), + withCapability(capabilityEphemeralOSDiskSupported, "True")), + } + got, _, err := selectVMSize(skus, testLocation, EphemeralOSDiskWorkerVMSizeSelector()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got != "Standard_E8as_v4" { + t.Fatalf("expected different-family SKU Standard_E8as_v4, got %q", got) + } +} From 6377f804ff6ff3cc88e9670e3f32efc6a56b988e Mon Sep 17 00:00:00 2001 From: Roi Vazquez Date: Tue, 14 Jul 2026 17:49:57 +0200 Subject: [PATCH 4/4] test(e2e): assert worker selector Preferred entries are allowlisted selectVMSize tries Preferred SKUs before discovery and does not constrain them by NamePattern, so a non-allowlisted SKU added to Preferred would bypass the allowlist boundary. Add a test asserting every production selector's Preferred entries also match its own NamePattern, keeping the two lists in sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/util/framework/vm_sku_selection_test.go | 23 ++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/util/framework/vm_sku_selection_test.go b/test/util/framework/vm_sku_selection_test.go index 58715a6d169..351de814332 100644 --- a/test/util/framework/vm_sku_selection_test.go +++ b/test/util/framework/vm_sku_selection_test.go @@ -465,3 +465,26 @@ func TestEphemeralSelectorSelectsDifferentFamily(t *testing.T) { t.Fatalf("expected different-family SKU Standard_E8as_v4, got %q", got) } } + +// TestWorkerSelectorPreferredEntriesAreAllowlisted closes the Preferred-list +// gap in the allowlist-boundary guard. selectVMSize tries Preferred SKUs before +// discovery and intentionally does NOT constrain them by NamePattern, so a +// non-allowlisted SKU added to Preferred would bypass the pattern boundary +// entirely. Asserting that every Preferred entry also matches its own +// NamePattern keeps the two lists in sync and prevents a non-allowlisted SKU +// from being reintroduced via Preferred. +func TestWorkerSelectorPreferredEntriesAreAllowlisted(t *testing.T) { + for _, sel := range productionWorkerSelectors() { + if sel.NamePattern == nil { + t.Fatalf("selector %q has no NamePattern; it is required as the allowlist boundary", sel.Name) + } + if len(sel.Preferred) == 0 { + t.Errorf("selector %q has no Preferred entries; expected an allowlisted default", sel.Name) + } + for _, name := range sel.Preferred { + if !sel.NamePattern.MatchString(name) { + t.Errorf("selector %q Preferred entry %q does not match its NamePattern %q; Preferred must stay within the RP allowlist", sel.Name, name, sel.NamePattern.String()) + } + } + } +}