From 0ff4f38d3cf0cdf62581ed4677152215f8a063e5 Mon Sep 17 00:00:00 2001
From: Philipp Matthes
Date: Tue, 28 Apr 2026 15:49:27 +0200
Subject: [PATCH 1/2] feat: e2e tests exercise all three feature modes via
per-request header override
The middleware now reads X-Cortex-Feature-Mode from every request and, when valid, stores it in context. featureModeFromConfOrHeader resolves the effective mode per handler (override if present, configured default otherwise). All handlers use this helper instead of reading config directly.
Each e2e test is wrapped with e2eWrapWithModes which iterates passthrough, hybrid, and crd. A custom RoundTripper (e2eModeTransport) auto-injects the header from context so individual tests need no changes. Passthrough-only endpoints probe for 501 via e2eProbeUnimplemented and skip gracefully. Tests that require CRD infrastructure (traits CRUD, RP traits writes) gate on the configured mode. The resourceLocker is now always initialized to avoid nil panics from mode overrides.
---
.../handle_allocation_candidates_e2e.go | 11 +-
.../shim/placement/handle_allocations_e2e.go | 11 +-
.../shim/placement/handle_reshaper_e2e.go | 11 +-
.../placement/handle_resource_classes_e2e.go | 11 +-
...handle_resource_provider_aggregates_e2e.go | 11 +-
...andle_resource_provider_allocations_e2e.go | 11 +-
...andle_resource_provider_inventories_e2e.go | 11 +-
.../handle_resource_provider_traits.go | 11 +-
.../handle_resource_provider_traits_e2e.go | 11 +-
.../handle_resource_provider_usages_e2e.go | 11 +-
.../placement/handle_resource_providers.go | 22 ++--
.../handle_resource_providers_e2e.go | 20 +++-
internal/shim/placement/handle_root.go | 2 +-
internal/shim/placement/handle_root_e2e.go | 2 +-
internal/shim/placement/handle_traits.go | 8 +-
internal/shim/placement/handle_traits_e2e.go | 93 ++++++++-------
internal/shim/placement/handle_usages_e2e.go | 11 +-
internal/shim/placement/shim.go | 40 +++++--
internal/shim/placement/shim_e2e.go | 98 +++++++++++++++-
internal/shim/placement/shim_io.go | 8 ++
internal/shim/placement/shim_test.go | 110 ++++++++++++++++++
21 files changed, 439 insertions(+), 85 deletions(-)
diff --git a/internal/shim/placement/handle_allocation_candidates_e2e.go b/internal/shim/placement/handle_allocation_candidates_e2e.go
index e90193224..c2dd4aacd 100644
--- a/internal/shim/placement/handle_allocation_candidates_e2e.go
+++ b/internal/shim/placement/handle_allocation_candidates_e2e.go
@@ -47,6 +47,15 @@ func e2eTestAllocationCandidates(ctx context.Context, _ client.Client) error {
const testRC = "CUSTOM_CORTEX_E2E_CAND_RC"
const apiVersion = "placement 1.26"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/allocation_candidates?resources=VCPU:1")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete leftover test resources from a prior run.
log.Info("Pre-cleanup: deleting leftover test resources")
for _, cleanup := range []struct {
@@ -296,5 +305,5 @@ func e2eTestAllocationCandidates(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "allocation_candidates", run: e2eTestAllocationCandidates})
+ e2eTests = append(e2eTests, e2eTest{name: "allocation_candidates", run: e2eWrapWithModes(e2eTestAllocationCandidates)})
}
diff --git a/internal/shim/placement/handle_allocations_e2e.go b/internal/shim/placement/handle_allocations_e2e.go
index 7f09a507b..27887ca7f 100644
--- a/internal/shim/placement/handle_allocations_e2e.go
+++ b/internal/shim/placement/handle_allocations_e2e.go
@@ -56,6 +56,15 @@ func e2eTestAllocations(ctx context.Context, _ client.Client) error {
const userID = "e2e50000-0000-0000-0000-000000000001"
const apiVersion = "placement 1.28"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/allocations/"+consumerUUID1)
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete allocations, resource provider, and resource class.
log.Info("Pre-cleanup: deleting leftover test resources")
for _, cleanup := range []struct {
@@ -476,5 +485,5 @@ func e2eTestAllocations(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "allocations", run: e2eTestAllocations})
+ e2eTests = append(e2eTests, e2eTest{name: "allocations", run: e2eWrapWithModes(e2eTestAllocations)})
}
diff --git a/internal/shim/placement/handle_reshaper_e2e.go b/internal/shim/placement/handle_reshaper_e2e.go
index f43809de4..29f84be34 100644
--- a/internal/shim/placement/handle_reshaper_e2e.go
+++ b/internal/shim/placement/handle_reshaper_e2e.go
@@ -57,6 +57,15 @@ func e2eTestReshaper(ctx context.Context, _ client.Client) error {
const userID = "e2e50000-0000-0000-0000-000000000001"
const apiVersion = "placement 1.30"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/allocations/"+consumerUUID)
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete allocation, both RPs, and custom resource class.
log.Info("Pre-cleanup: deleting leftover test resources")
for _, cleanup := range []struct {
@@ -571,5 +580,5 @@ func e2eTestReshaper(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "reshaper", run: e2eTestReshaper})
+ e2eTests = append(e2eTests, e2eTest{name: "reshaper", run: e2eWrapWithModes(e2eTestReshaper)})
}
diff --git a/internal/shim/placement/handle_resource_classes_e2e.go b/internal/shim/placement/handle_resource_classes_e2e.go
index 360e1ef80..e848ee034 100644
--- a/internal/shim/placement/handle_resource_classes_e2e.go
+++ b/internal/shim/placement/handle_resource_classes_e2e.go
@@ -42,6 +42,15 @@ func e2eTestResourceClasses(ctx context.Context, _ client.Client) error {
const testRC = "CUSTOM_CORTEX_E2E_RC"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/resource_classes")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete any leftover test resource class from a prior run.
log.Info("Pre-cleanup: deleting leftover test resource class", "class", testRC)
req, err := http.NewRequestWithContext(ctx,
@@ -226,5 +235,5 @@ func e2eTestResourceClasses(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_classes", run: e2eTestResourceClasses})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_classes", run: e2eWrapWithModes(e2eTestResourceClasses)})
}
diff --git a/internal/shim/placement/handle_resource_provider_aggregates_e2e.go b/internal/shim/placement/handle_resource_provider_aggregates_e2e.go
index b673c75f6..7eb6ba089 100644
--- a/internal/shim/placement/handle_resource_provider_aggregates_e2e.go
+++ b/internal/shim/placement/handle_resource_provider_aggregates_e2e.go
@@ -48,6 +48,15 @@ func e2eTestResourceProviderAggregates(ctx context.Context, _ client.Client) err
const testAggUUID1 = "e2e30000-0000-0000-0000-000000000001"
const testAggUUID2 = "e2e30000-0000-0000-0000-000000000002"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/resource_providers/"+testRPUUID+"/aggregates")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete any leftover test resource provider from a prior run.
log.Info("Pre-cleanup: deleting leftover test resource provider", "uuid", testRPUUID)
req, err := http.NewRequestWithContext(ctx,
@@ -346,5 +355,5 @@ func e2eTestResourceProviderAggregates(ctx context.Context, _ client.Client) err
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_provider_aggregates", run: e2eTestResourceProviderAggregates})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_provider_aggregates", run: e2eWrapWithModes(e2eTestResourceProviderAggregates)})
}
diff --git a/internal/shim/placement/handle_resource_provider_allocations_e2e.go b/internal/shim/placement/handle_resource_provider_allocations_e2e.go
index a63c8cb4f..aea84ec06 100644
--- a/internal/shim/placement/handle_resource_provider_allocations_e2e.go
+++ b/internal/shim/placement/handle_resource_provider_allocations_e2e.go
@@ -44,6 +44,15 @@ func e2eTestResourceProviderAllocations(ctx context.Context, _ client.Client) er
const testRPUUID = "e2e10000-0000-0000-0000-000000000006"
const testRPName = "cortex-e2e-test-rp-alloc-view"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/resource_providers/"+testRPUUID+"/allocations")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete any leftover test resource provider from a prior run.
log.Info("Pre-cleanup: deleting leftover test resource provider", "uuid", testRPUUID)
req, err := http.NewRequestWithContext(ctx,
@@ -227,5 +236,5 @@ func e2eTestResourceProviderAllocations(ctx context.Context, _ client.Client) er
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_provider_allocations", run: e2eTestResourceProviderAllocations})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_provider_allocations", run: e2eWrapWithModes(e2eTestResourceProviderAllocations)})
}
diff --git a/internal/shim/placement/handle_resource_provider_inventories_e2e.go b/internal/shim/placement/handle_resource_provider_inventories_e2e.go
index 354460e81..1462ca87b 100644
--- a/internal/shim/placement/handle_resource_provider_inventories_e2e.go
+++ b/internal/shim/placement/handle_resource_provider_inventories_e2e.go
@@ -53,6 +53,15 @@ func e2eTestResourceProviderInventories(ctx context.Context, _ client.Client) er
const testRC = "CUSTOM_CORTEX_E2E_INV_RC"
const apiVersion = "placement 1.26"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/resource_providers/"+testRPUUID+"/inventories")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete the resource provider (cascades inventories), then
// the custom resource class. Ignore 404/409.
log.Info("Pre-cleanup: deleting leftover test resources")
@@ -488,5 +497,5 @@ func e2eTestResourceProviderInventories(ctx context.Context, _ client.Client) er
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_provider_inventories", run: e2eTestResourceProviderInventories})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_provider_inventories", run: e2eWrapWithModes(e2eTestResourceProviderInventories)})
}
diff --git a/internal/shim/placement/handle_resource_provider_traits.go b/internal/shim/placement/handle_resource_provider_traits.go
index 16978a593..b23ac8e59 100644
--- a/internal/shim/placement/handle_resource_provider_traits.go
+++ b/internal/shim/placement/handle_resource_provider_traits.go
@@ -4,7 +4,6 @@
package placement
import (
- "fmt"
"net/http"
hv1 "github.com/cobaltcore-dev/openstack-hypervisor-operator/api/v1"
@@ -33,7 +32,7 @@ func (s *Shim) HandleListResourceProviderTraits(w http.ResponseWriter, r *http.R
if !ok {
return
}
- switch s.config.Features.ResourceProviderTraits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviderTraits) {
case FeatureModePassthrough:
s.forward(w, r)
case FeatureModeHybrid:
@@ -92,13 +91,13 @@ func (s *Shim) HandleUpdateResourceProviderTraits(w http.ResponseWriter, r *http
if _, ok := requiredUUIDPathParam(w, r, "uuid"); !ok {
return
}
- switch s.config.Features.ResourceProviderTraits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviderTraits) {
case FeatureModePassthrough:
s.forward(w, r)
case FeatureModeHybrid:
s.forward(w, r)
case FeatureModeCRD:
- http.Error(w, fmt.Sprintf("%s mode is not yet implemented for resource provider trait writes", s.config.Features.ResourceProviderTraits), http.StatusNotImplemented)
+ http.Error(w, "crd mode is not yet implemented for resource provider trait writes", http.StatusNotImplemented)
default:
http.Error(w, "unknown feature mode", http.StatusInternalServerError)
}
@@ -117,13 +116,13 @@ func (s *Shim) HandleDeleteResourceProviderTraits(w http.ResponseWriter, r *http
if _, ok := requiredUUIDPathParam(w, r, "uuid"); !ok {
return
}
- switch s.config.Features.ResourceProviderTraits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviderTraits) {
case FeatureModePassthrough:
s.forward(w, r)
case FeatureModeHybrid:
s.forward(w, r)
case FeatureModeCRD:
- http.Error(w, fmt.Sprintf("%s mode is not yet implemented for resource provider trait writes", s.config.Features.ResourceProviderTraits), http.StatusNotImplemented)
+ http.Error(w, "crd mode is not yet implemented for resource provider trait writes", http.StatusNotImplemented)
default:
http.Error(w, "unknown feature mode", http.StatusInternalServerError)
}
diff --git a/internal/shim/placement/handle_resource_provider_traits_e2e.go b/internal/shim/placement/handle_resource_provider_traits_e2e.go
index c697ca7ff..4acd665b0 100644
--- a/internal/shim/placement/handle_resource_provider_traits_e2e.go
+++ b/internal/shim/placement/handle_resource_provider_traits_e2e.go
@@ -42,6 +42,15 @@ func e2eTestResourceProviderTraits(ctx context.Context, _ client.Client) error {
}
log.Info("Successfully created openstack client for resource provider traits e2e test")
+ // Resource provider trait writes (PUT/DELETE) are not yet implemented in
+ // crd mode, and the test RP created via POST won't exist as a Hypervisor
+ // CRD either, so skip the entire test in crd mode.
+ rpTraitsMode := e2eCurrentMode(ctx)
+ if rpTraitsMode == FeatureModeCRD {
+ log.Info("Skipping resource provider traits e2e test because mode is crd (writes not implemented)")
+ return nil
+ }
+
const testRPUUID = "e2e10000-0000-0000-0000-000000000003"
const testRPName = "cortex-e2e-test-rp-traits"
const testTrait = "CUSTOM_CORTEX_E2E_RP_TRAIT"
@@ -382,5 +391,5 @@ func e2eTestResourceProviderTraits(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_provider_traits", run: e2eTestResourceProviderTraits})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_provider_traits", run: e2eWrapWithModes(e2eTestResourceProviderTraits)})
}
diff --git a/internal/shim/placement/handle_resource_provider_usages_e2e.go b/internal/shim/placement/handle_resource_provider_usages_e2e.go
index c548162ba..05965b23c 100644
--- a/internal/shim/placement/handle_resource_provider_usages_e2e.go
+++ b/internal/shim/placement/handle_resource_provider_usages_e2e.go
@@ -44,6 +44,15 @@ func e2eTestResourceProviderUsages(ctx context.Context, _ client.Client) error {
const testRPUUID = "e2e10000-0000-0000-0000-000000000005"
const testRPName = "cortex-e2e-test-rp-usages"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/resource_providers/"+testRPUUID+"/usages")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Pre-cleanup: delete any leftover test resource provider from a prior run.
log.Info("Pre-cleanup: deleting leftover test resource provider", "uuid", testRPUUID)
req, err := http.NewRequestWithContext(ctx,
@@ -227,5 +236,5 @@ func e2eTestResourceProviderUsages(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_provider_usages", run: e2eTestResourceProviderUsages})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_provider_usages", run: e2eWrapWithModes(e2eTestResourceProviderUsages)})
}
diff --git a/internal/shim/placement/handle_resource_providers.go b/internal/shim/placement/handle_resource_providers.go
index 7d4dd1fff..04955fd72 100644
--- a/internal/shim/placement/handle_resource_providers.go
+++ b/internal/shim/placement/handle_resource_providers.go
@@ -115,7 +115,8 @@ func (s *Shim) HandleCreateResourceProvider(w http.ResponseWriter, r *http.Reque
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.ResourceProviders.orDefault() {
+ mode := s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviders)
+ switch mode {
case FeatureModePassthrough:
s.forward(w, r)
return
@@ -184,7 +185,7 @@ func (s *Shim) HandleCreateResourceProvider(w http.ResponseWriter, r *http.Reque
}
// No conflict — forward to upstream placement (hybrid) or reject (crd).
- if s.config.Features.ResourceProviders.orDefault() == FeatureModeCRD {
+ if mode == FeatureModeCRD {
log.Info("crd mode: non-kvm resource provider create not supported", "name", req.Name)
http.Error(w, "resource provider not found", http.StatusNotFound)
return
@@ -209,7 +210,8 @@ func (s *Shim) HandleShowResourceProvider(w http.ResponseWriter, r *http.Request
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.ResourceProviders.orDefault() {
+ mode := s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviders)
+ switch mode {
case FeatureModePassthrough:
s.forward(w, r)
return
@@ -229,7 +231,7 @@ func (s *Shim) HandleShowResourceProvider(w http.ResponseWriter, r *http.Request
var hvs hv1.HypervisorList
err := s.List(ctx, &hvs, client.MatchingFields{idxHypervisorOpenStackId: uuid})
if apierrors.IsNotFound(err) || len(hvs.Items) == 0 {
- if s.config.Features.ResourceProviders.orDefault() == FeatureModeCRD {
+ if mode == FeatureModeCRD {
log.Info("resource provider not found in kubernetes (crd mode)", "uuid", uuid)
http.Error(w, "resource provider not found", http.StatusNotFound)
return
@@ -278,7 +280,8 @@ func (s *Shim) HandleUpdateResourceProvider(w http.ResponseWriter, r *http.Reque
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.ResourceProviders.orDefault() {
+ mode := s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviders)
+ switch mode {
case FeatureModePassthrough:
s.forward(w, r)
return
@@ -315,7 +318,7 @@ func (s *Shim) HandleUpdateResourceProvider(w http.ResponseWriter, r *http.Reque
var hvs hv1.HypervisorList
err = s.List(ctx, &hvs, client.MatchingFields{idxHypervisorOpenStackId: uuid})
if apierrors.IsNotFound(err) || len(hvs.Items) == 0 {
- if s.config.Features.ResourceProviders.orDefault() == FeatureModeCRD {
+ if mode == FeatureModeCRD {
log.Info("resource provider not found in kubernetes (crd mode)", "uuid", uuid)
http.Error(w, "resource provider not found", http.StatusNotFound)
return
@@ -373,7 +376,8 @@ func (s *Shim) HandleDeleteResourceProvider(w http.ResponseWriter, r *http.Reque
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.ResourceProviders.orDefault() {
+ mode := s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviders)
+ switch mode {
case FeatureModePassthrough:
s.forward(w, r)
return
@@ -393,7 +397,7 @@ func (s *Shim) HandleDeleteResourceProvider(w http.ResponseWriter, r *http.Reque
var hvs hv1.HypervisorList
err := s.List(ctx, &hvs, client.MatchingFields{idxHypervisorOpenStackId: uuid})
if apierrors.IsNotFound(err) || len(hvs.Items) == 0 {
- if s.config.Features.ResourceProviders.orDefault() == FeatureModeCRD {
+ if mode == FeatureModeCRD {
log.Info("resource provider not found in kubernetes (crd mode)", "uuid", uuid)
http.Error(w, "resource provider not found", http.StatusNotFound)
return
@@ -448,7 +452,7 @@ type listResourceProvidersResponse struct {
//
// See: https://docs.openstack.org/api-ref/placement/#list-resource-providers
func (s *Shim) HandleListResourceProviders(w http.ResponseWriter, r *http.Request) {
- switch s.config.Features.ResourceProviders.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.ResourceProviders) {
case FeatureModePassthrough:
s.forward(w, r)
case FeatureModeHybrid:
diff --git a/internal/shim/placement/handle_resource_providers_e2e.go b/internal/shim/placement/handle_resource_providers_e2e.go
index 90b850369..2faca2fc2 100644
--- a/internal/shim/placement/handle_resource_providers_e2e.go
+++ b/internal/shim/placement/handle_resource_providers_e2e.go
@@ -58,14 +58,24 @@ func e2eTestResourceProviders(ctx context.Context, cl client.Client) error {
// ==================== Phase 1: VMware path ====================
- log.Info("=== VMware path: passthrough resource provider tests ===")
- if err := e2eVMwareResourceProviders(ctx, sc); err != nil {
- return fmt.Errorf("VMware path: %w", err)
+ // The VMware path creates synthetic test RPs against upstream placement.
+ // In crd mode there is no upstream, so skip it.
+ mode := e2eCurrentMode(ctx)
+ if mode == "" {
+ mode = config.Features.ResourceProviders.orDefault()
+ }
+ if mode != FeatureModeCRD {
+ log.Info("=== VMware path: passthrough resource provider tests ===")
+ if err := e2eVMwareResourceProviders(ctx, sc); err != nil {
+ return fmt.Errorf("VMware path: %w", err)
+ }
+ } else {
+ log.Info("Skipping VMware path because mode is crd (no upstream placement)")
}
// ==================== Phase 2: KVM path ====================
- if config.Features.ResourceProviders.orDefault() == FeatureModePassthrough {
+ if mode == FeatureModePassthrough {
log.Info("Skipping KVM resource provider e2e tests because resourceProviders mode is passthrough")
} else {
log.Info("=== KVM path: hypervisor-backed resource provider tests ===")
@@ -506,5 +516,5 @@ func e2eKVMResourceProviders(ctx context.Context, sc *gophercloud.ServiceClient,
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "resource_providers", run: e2eTestResourceProviders})
+ e2eTests = append(e2eTests, e2eTest{name: "resource_providers", run: e2eWrapWithModes(e2eTestResourceProviders)})
}
diff --git a/internal/shim/placement/handle_root.go b/internal/shim/placement/handle_root.go
index acad69dcb..9f9b510e7 100644
--- a/internal/shim/placement/handle_root.go
+++ b/internal/shim/placement/handle_root.go
@@ -50,7 +50,7 @@ func (s *Shim) HandleGetRoot(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.Root.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.Root) {
case FeatureModePassthrough:
log.Info("forwarding GET / to upstream placement")
s.forward(w, r)
diff --git a/internal/shim/placement/handle_root_e2e.go b/internal/shim/placement/handle_root_e2e.go
index 2e558705a..e4a785606 100644
--- a/internal/shim/placement/handle_root_e2e.go
+++ b/internal/shim/placement/handle_root_e2e.go
@@ -51,5 +51,5 @@ func e2eTestGetRoot(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "root", run: e2eTestGetRoot})
+ e2eTests = append(e2eTests, e2eTest{name: "root", run: e2eWrapWithModes(e2eTestGetRoot)})
}
diff --git a/internal/shim/placement/handle_traits.go b/internal/shim/placement/handle_traits.go
index b509b2599..429b87cfc 100644
--- a/internal/shim/placement/handle_traits.go
+++ b/internal/shim/placement/handle_traits.go
@@ -62,7 +62,7 @@ func (s *Shim) HandleListTraits(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.Traits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.Traits) {
case FeatureModePassthrough, FeatureModeHybrid:
s.forward(w, r)
return
@@ -132,7 +132,7 @@ func (s *Shim) HandleShowTrait(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.Traits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.Traits) {
case FeatureModePassthrough, FeatureModeHybrid:
s.forward(w, r)
return
@@ -174,7 +174,7 @@ func (s *Shim) HandleUpdateTrait(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.Traits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.Traits) {
case FeatureModePassthrough, FeatureModeHybrid:
s.forward(w, r)
return
@@ -298,7 +298,7 @@ func (s *Shim) HandleDeleteTrait(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log := logf.FromContext(ctx)
- switch s.config.Features.Traits.orDefault() {
+ switch s.featureModeFromConfOrHeader(r, s.config.Features.Traits) {
case FeatureModePassthrough, FeatureModeHybrid:
s.forward(w, r)
return
diff --git a/internal/shim/placement/handle_traits_e2e.go b/internal/shim/placement/handle_traits_e2e.go
index 8a904b935..4a5831f72 100644
--- a/internal/shim/placement/handle_traits_e2e.go
+++ b/internal/shim/placement/handle_traits_e2e.go
@@ -83,7 +83,11 @@ func e2eTestTraits(ctx context.Context, _ client.Client) error {
// When traits are served locally (hybrid or crd mode) the static list may
// be empty. Only require at least one trait when forwarding to upstream
// placement, which always has standard traits.
- if config.Features.Traits.orDefault() == FeatureModePassthrough && len(listResp.Traits) == 0 {
+ traitsMode := e2eCurrentMode(ctx)
+ if traitsMode == "" {
+ traitsMode = config.Features.Traits.orDefault()
+ }
+ if traitsMode == FeatureModePassthrough && len(listResp.Traits) == 0 {
return errors.New("GET /traits: expected at least one trait, got 0")
}
log.Info("Successfully retrieved traits", "count", len(listResp.Traits))
@@ -133,8 +137,13 @@ func e2eTestTraits(ctx context.Context, _ client.Client) error {
// ==================== Phase 2: CRUD tests (feature-gated) ====================
- if config.Features.Traits.orDefault() == FeatureModePassthrough {
- log.Info("Skipping trait CRUD e2e tests because traits mode is passthrough")
+ // CRUD tests require traits ConfigMaps which are only created when the
+ // configured traits mode is hybrid or crd. The override header changes
+ // handler routing but cannot create ConfigMaps that don't exist.
+ configuredTraitsMode := config.Features.Traits.orDefault()
+ if traitsMode == FeatureModePassthrough || configuredTraitsMode == FeatureModePassthrough {
+ log.Info("Skipping trait CRUD e2e tests",
+ "overrideMode", traitsMode, "configuredMode", configuredTraitsMode)
return nil
}
@@ -287,47 +296,53 @@ func e2eTestTraits(ctx context.Context, _ client.Client) error {
}
log.Info("Verified test trait was deleted", "trait", testTrait)
- // Test PUT /traits/{name} with bad prefix → 400.
- log.Info("Testing PUT /traits/{name} with non-CUSTOM_ prefix")
- req, err = http.NewRequestWithContext(ctx,
- http.MethodPut, sc.Endpoint+"/traits/HW_CORTEX_E2E_BAD", http.NoBody)
- if err != nil {
- return fmt.Errorf("failed to create bad-prefix PUT request: %w", err)
- }
- req.Header.Set("X-Auth-Token", sc.TokenID)
- req.Header.Set("OpenStack-API-Version", "placement 1.6")
- resp, err = sc.HTTPClient.Do(req)
- if err != nil {
- return fmt.Errorf("failed to send bad-prefix PUT request: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusBadRequest {
- return fmt.Errorf("PUT /traits/HW_CORTEX_E2E_BAD: expected 400, got %d", resp.StatusCode)
- }
- log.Info("Correctly received 400 for PUT with non-CUSTOM_ prefix")
+ // Bad-prefix validation is only enforced by the shim in crd mode.
+ // In hybrid mode, writes forward to upstream which has different behavior.
+ if traitsMode == FeatureModeCRD {
+ // Test PUT /traits/{name} with bad prefix → 400.
+ log.Info("Testing PUT /traits/{name} with non-CUSTOM_ prefix")
+ req, err = http.NewRequestWithContext(ctx,
+ http.MethodPut, sc.Endpoint+"/traits/HW_CORTEX_E2E_BAD", http.NoBody)
+ if err != nil {
+ return fmt.Errorf("failed to create bad-prefix PUT request: %w", err)
+ }
+ req.Header.Set("X-Auth-Token", sc.TokenID)
+ req.Header.Set("OpenStack-API-Version", "placement 1.6")
+ resp, err = sc.HTTPClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send bad-prefix PUT request: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusBadRequest {
+ return fmt.Errorf("PUT /traits/HW_CORTEX_E2E_BAD: expected 400, got %d", resp.StatusCode)
+ }
+ log.Info("Correctly received 400 for PUT with non-CUSTOM_ prefix")
- // Test DELETE /traits/{name} with bad prefix → 400.
- log.Info("Testing DELETE /traits/{name} with non-CUSTOM_ prefix")
- req, err = http.NewRequestWithContext(ctx,
- http.MethodDelete, sc.Endpoint+"/traits/HW_CORTEX_E2E_BAD", http.NoBody)
- if err != nil {
- return fmt.Errorf("failed to create bad-prefix DELETE request: %w", err)
- }
- req.Header.Set("X-Auth-Token", sc.TokenID)
- req.Header.Set("OpenStack-API-Version", "placement 1.6")
- resp, err = sc.HTTPClient.Do(req)
- if err != nil {
- return fmt.Errorf("failed to send bad-prefix DELETE request: %w", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusBadRequest {
- return fmt.Errorf("DELETE /traits/HW_CORTEX_E2E_BAD: expected 400, got %d", resp.StatusCode)
+ // Test DELETE /traits/{name} with bad prefix → 400.
+ log.Info("Testing DELETE /traits/{name} with non-CUSTOM_ prefix")
+ req, err = http.NewRequestWithContext(ctx,
+ http.MethodDelete, sc.Endpoint+"/traits/HW_CORTEX_E2E_BAD", http.NoBody)
+ if err != nil {
+ return fmt.Errorf("failed to create bad-prefix DELETE request: %w", err)
+ }
+ req.Header.Set("X-Auth-Token", sc.TokenID)
+ req.Header.Set("OpenStack-API-Version", "placement 1.6")
+ resp, err = sc.HTTPClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send bad-prefix DELETE request: %w", err)
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusBadRequest {
+ return fmt.Errorf("DELETE /traits/HW_CORTEX_E2E_BAD: expected 400, got %d", resp.StatusCode)
+ }
+ log.Info("Correctly received 400 for DELETE with non-CUSTOM_ prefix")
+ } else {
+ log.Info("Skipping bad-prefix validation tests (only enforced in crd mode)")
}
- log.Info("Correctly received 400 for DELETE with non-CUSTOM_ prefix")
return nil
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "traits", run: e2eTestTraits})
+ e2eTests = append(e2eTests, e2eTest{name: "traits", run: e2eWrapWithModes(e2eTestTraits)})
}
diff --git a/internal/shim/placement/handle_usages_e2e.go b/internal/shim/placement/handle_usages_e2e.go
index c7ac8c965..66f5c40a9 100644
--- a/internal/shim/placement/handle_usages_e2e.go
+++ b/internal/shim/placement/handle_usages_e2e.go
@@ -39,6 +39,15 @@ func e2eTestUsages(ctx context.Context, _ client.Client) error {
const apiVersion = "placement 1.9"
+ // Probe: for non-passthrough modes, verify endpoint returns 501.
+ unimplemented, err := e2eProbeUnimplemented(ctx, sc, sc.Endpoint+"/usages?project_id=test")
+ if err != nil {
+ return fmt.Errorf("probe: %w", err)
+ }
+ if unimplemented {
+ return nil
+ }
+
// Get the list of projects from the identity service, so that we can test
// the /usages endpoint with a valid project id.
log.Info("Getting list of projects from identity service for usages e2e test")
@@ -113,5 +122,5 @@ func e2eTestUsages(ctx context.Context, _ client.Client) error {
}
func init() {
- e2eTests = append(e2eTests, e2eTest{name: "usages", run: e2eTestUsages})
+ e2eTests = append(e2eTests, e2eTest{name: "usages", run: e2eWrapWithModes(e2eTestUsages)})
}
diff --git a/internal/shim/placement/shim.go b/internal/shim/placement/shim.go
index 273273983..4e09c56c3 100644
--- a/internal/shim/placement/shim.go
+++ b/internal/shim/placement/shim.go
@@ -52,6 +52,18 @@ type requestIDContextKey struct{}
// header value through the request lifecycle for tracing.
var requestIDKey = requestIDContextKey{}
+// featureModeOverrideContextKey is a separate type for the per-request feature
+// mode override injected via the X-Cortex-Feature-Mode header.
+type featureModeOverrideContextKey struct{}
+
+// featureModeOverrideKey is the context key used to propagate the feature mode
+// override from the middleware to handlers.
+var featureModeOverrideKey = featureModeOverrideContextKey{}
+
+// headerFeatureModeOverride is the HTTP header that allows e2e tests to
+// override the configured feature mode on a per-request basis.
+const headerFeatureModeOverride = "X-Cortex-Feature-Mode"
+
// FeatureMode controls how an endpoint group interacts with upstream
// placement and the hypervisor CRD.
type FeatureMode string
@@ -90,16 +102,29 @@ func (m FeatureMode) valid() bool {
// dispatchPassthroughOnly forwards in passthrough mode, returns 501 for
// hybrid/crd, and 500 for unknown modes.
func (s *Shim) dispatchPassthroughOnly(w http.ResponseWriter, r *http.Request, mode FeatureMode) {
- switch mode.orDefault() {
+ resolved := s.featureModeFromConfOrHeader(r, mode)
+ switch resolved {
case FeatureModePassthrough:
s.forward(w, r)
case FeatureModeHybrid, FeatureModeCRD:
- http.Error(w, fmt.Sprintf("%s mode is not yet implemented for this endpoint", mode), http.StatusNotImplemented)
+ http.Error(w, fmt.Sprintf("%s mode is not yet implemented for this endpoint", resolved), http.StatusNotImplemented)
default:
http.Error(w, "unknown feature mode", http.StatusInternalServerError)
}
}
+// featureModeFromConfOrHeader returns the effective feature mode for the
+// current request. If a valid override is present in the request context
+// (injected by wrapHandler from the X-Cortex-Feature-Mode header), the
+// override takes precedence. Otherwise the configured mode's default is
+// returned.
+func (s *Shim) featureModeFromConfOrHeader(r *http.Request, configured FeatureMode) FeatureMode {
+ if override, ok := r.Context().Value(featureModeOverrideKey).(FeatureMode); ok {
+ return override.orDefault()
+ }
+ return configured.orDefault()
+}
+
// featuresConfig controls the feature mode for each endpoint group.
// Every field defaults to passthrough (zero value) when omitted.
type featuresConfig struct {
@@ -472,13 +497,10 @@ func (s *Shim) SetupWithManager(ctx context.Context, mgr ctrl.Manager) (err erro
Buckets: prometheus.DefBuckets,
}, []string{"method", "pattern", "responsecode"})
- traitsMode := s.config.Features.Traits.orDefault()
- if traitsMode == FeatureModeHybrid || traitsMode == FeatureModeCRD {
- s.resourceLocker = resourcelock.NewResourceLocker(
- s.Client,
- os.Getenv("POD_NAMESPACE"),
- )
- }
+ s.resourceLocker = resourcelock.NewResourceLocker(
+ s.Client,
+ os.Getenv("POD_NAMESPACE"),
+ )
// Check that the provided client is a multicluster client, since we need
// that to watch for hypervisors across clusters.
diff --git a/internal/shim/placement/shim_e2e.go b/internal/shim/placement/shim_e2e.go
index e7f9e30f3..553912448 100644
--- a/internal/shim/placement/shim_e2e.go
+++ b/internal/shim/placement/shim_e2e.go
@@ -66,7 +66,7 @@ func makeE2EServiceClient(ctx context.Context, rc e2eRootConfig) (*gophercloud.S
log.Info("No SSO config provided, using plain transport for placement API")
transport = &http.Transport{}
}
- provider.HTTPClient.Transport = transport
+ provider.HTTPClient.Transport = &e2eModeTransport{base: transport}
if err := openstack.Authenticate(ctx, provider, authOpts); err != nil {
log.Error(err, "Failed to authenticate with keystone")
return nil, fmt.Errorf("failed to authenticate with keystone: %w", err)
@@ -88,6 +88,102 @@ type e2eTest struct {
// e2eTests is populated by init() functions in the handle_*_e2e.go files.
var e2eTests []e2eTest
+// e2eAllModes is the list of feature modes exercised by e2e tests when
+// AllowModeOverride is enabled.
+var e2eAllModes = []FeatureMode{
+ FeatureModePassthrough,
+ FeatureModeHybrid,
+ FeatureModeCRD,
+}
+
+// setFeatureModeHeader sets the X-Cortex-Feature-Mode override header on the
+// request so the shim dispatches to the specified mode regardless of its
+// configured mode.
+func setFeatureModeHeader(req *http.Request, mode FeatureMode) {
+ if mode != "" {
+ req.Header.Set(headerFeatureModeOverride, string(mode))
+ }
+}
+
+// e2eModeContextKey is used to pass the current test mode through context.
+type e2eModeContextKey struct{}
+
+// e2eCurrentMode retrieves the feature mode from context (set by
+// e2eWrapWithModes). Returns empty string if not set.
+func e2eCurrentMode(ctx context.Context) FeatureMode {
+ if m, ok := ctx.Value(e2eModeContextKey{}).(FeatureMode); ok {
+ return m
+ }
+ return ""
+}
+
+// e2eWrapWithModes returns a test function that iterates over all feature
+// modes. For each mode it injects the mode into context (retrievable via
+// e2eCurrentMode) so that the e2eModeTransport sets the override header on
+// every outgoing request.
+func e2eWrapWithModes(fn func(ctx context.Context, cl client.Client) error) func(ctx context.Context, cl client.Client) error {
+ return func(ctx context.Context, cl client.Client) error {
+ log := logf.FromContext(ctx)
+ for _, mode := range e2eAllModes {
+ modeLog := log.WithName(string(mode))
+ modeCtx := context.WithValue(ctx, e2eModeContextKey{}, mode)
+ modeCtx = logf.IntoContext(modeCtx, modeLog)
+ modeLog.Info("Starting mode")
+ if err := fn(modeCtx, cl); err != nil {
+ return fmt.Errorf("mode %s: %w", mode, err)
+ }
+ modeLog.Info("Mode passed")
+ }
+ return nil
+ }
+}
+
+// e2eProbeUnimplemented sends a single GET request with the mode override
+// header to verify the endpoint returns 501 Not Implemented. Returns true if
+// the endpoint is unimplemented for this mode (test should skip). Returns
+// false if the endpoint works (test should continue). Returns an error if
+// something unexpected happened.
+func e2eProbeUnimplemented(ctx context.Context, sc *gophercloud.ServiceClient, probeURL string) (bool, error) {
+ log := logf.FromContext(ctx)
+ mode := e2eCurrentMode(ctx)
+ if mode == "" || mode == FeatureModePassthrough {
+ return false, nil
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, probeURL, http.NoBody)
+ if err != nil {
+ return false, err
+ }
+ req.Header.Set("X-Auth-Token", sc.TokenID)
+ req.Header.Set("OpenStack-API-Version", "placement 1.6")
+ setFeatureModeHeader(req, mode)
+ resp, err := sc.HTTPClient.Do(req)
+ if err != nil {
+ return false, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode == http.StatusNotImplemented {
+ log.Info("Endpoint correctly returns 501 for unimplemented mode", "mode", mode)
+ return true, nil
+ }
+ return false, nil
+}
+
+// e2eModeTransport wraps an http.RoundTripper to automatically inject the
+// X-Cortex-Feature-Mode header based on the mode stored in the request's
+// context (via e2eModeContextKey). This avoids manually calling
+// setFeatureModeHeader on every request in every e2e test.
+type e2eModeTransport struct {
+ base http.RoundTripper
+}
+
+func (t *e2eModeTransport) RoundTrip(req *http.Request) (*http.Response, error) {
+ if mode := e2eCurrentMode(req.Context()); mode != "" {
+ req = req.Clone(req.Context())
+ req.Header.Set(headerFeatureModeOverride, string(mode))
+ }
+ return t.base.RoundTrip(req)
+}
+
// RunE2E executes end-to-end tests for all placement shim handlers.
// It stops on the first failure and returns the error.
func RunE2E(ctx context.Context, cl client.Client) error {
diff --git a/internal/shim/placement/shim_io.go b/internal/shim/placement/shim_io.go
index 98d5ba0bc..792f8edc3 100644
--- a/internal/shim/placement/shim_io.go
+++ b/internal/shim/placement/shim_io.go
@@ -113,6 +113,14 @@ func (s *Shim) wrapHandler(pattern string, next http.HandlerFunc) http.HandlerFu
log = log.WithValues("requestID", reqID)
ctx = context.WithValue(ctx, requestIDKey, reqID)
}
+
+ // Read the feature mode override header and store in context.
+ if raw := r.Header.Get(headerFeatureModeOverride); raw != "" {
+ if fm := FeatureMode(raw); fm.valid() && fm != "" {
+ ctx = context.WithValue(ctx, featureModeOverrideKey, fm)
+ }
+ }
+
ctx = logf.IntoContext(ctx, log)
r = r.WithContext(ctx)
diff --git a/internal/shim/placement/shim_test.go b/internal/shim/placement/shim_test.go
index 503b94c72..2e75e6645 100644
--- a/internal/shim/placement/shim_test.go
+++ b/internal/shim/placement/shim_test.go
@@ -561,3 +561,113 @@ func TestWrapHandlerWithAuth(t *testing.T) {
}
})
}
+
+func TestFeatureModeFromConfOrHeader(t *testing.T) {
+ s := &Shim{}
+
+ t.Run("returns configured mode when no override", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ got := s.featureModeFromConfOrHeader(req, FeatureModeHybrid)
+ if got != FeatureModeHybrid {
+ t.Fatalf("got %q, want %q", got, FeatureModeHybrid)
+ }
+ })
+
+ t.Run("defaults empty configured mode to passthrough", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ got := s.featureModeFromConfOrHeader(req, "")
+ if got != FeatureModePassthrough {
+ t.Fatalf("got %q, want %q", got, FeatureModePassthrough)
+ }
+ })
+
+ t.Run("returns override when present in context", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureModeCRD)
+ req = req.WithContext(ctx)
+ got := s.featureModeFromConfOrHeader(req, FeatureModePassthrough)
+ if got != FeatureModeCRD {
+ t.Fatalf("got %q, want %q", got, FeatureModeCRD)
+ }
+ })
+
+ t.Run("override defaults empty to passthrough", func(t *testing.T) {
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureMode(""))
+ req = req.WithContext(ctx)
+ got := s.featureModeFromConfOrHeader(req, FeatureModeHybrid)
+ if got != FeatureModePassthrough {
+ t.Fatalf("got %q, want %q", got, FeatureModePassthrough)
+ }
+ })
+}
+
+func TestWrapHandlerFeatureModeOverride(t *testing.T) {
+ t.Run("valid header injects override into context", func(t *testing.T) {
+ var gotMode FeatureMode
+ down, up := newTestTimers()
+ s := &Shim{
+ config: config{PlacementURL: "http://unused"},
+ maxBodyLogSize: 4096,
+ downstreamRequestTimer: down,
+ upstreamRequestTimer: up,
+ }
+ wrapped := s.wrapHandler("/test", func(w http.ResponseWriter, r *http.Request) {
+ if override, ok := r.Context().Value(featureModeOverrideKey).(FeatureMode); ok {
+ gotMode = override
+ }
+ w.WriteHeader(http.StatusOK)
+ })
+ req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
+ req.Header.Set(headerFeatureModeOverride, string(FeatureModeCRD))
+ w := httptest.NewRecorder()
+ wrapped(w, req)
+ if gotMode != FeatureModeCRD {
+ t.Fatalf("context override = %q, want %q", gotMode, FeatureModeCRD)
+ }
+ })
+
+ t.Run("invalid header value is ignored", func(t *testing.T) {
+ var gotOverride bool
+ down, up := newTestTimers()
+ s := &Shim{
+ config: config{PlacementURL: "http://unused"},
+ maxBodyLogSize: 4096,
+ downstreamRequestTimer: down,
+ upstreamRequestTimer: up,
+ }
+ wrapped := s.wrapHandler("/test", func(w http.ResponseWriter, r *http.Request) {
+ _, gotOverride = r.Context().Value(featureModeOverrideKey).(FeatureMode)
+ w.WriteHeader(http.StatusOK)
+ })
+ req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
+ req.Header.Set(headerFeatureModeOverride, "bogus")
+ w := httptest.NewRecorder()
+ wrapped(w, req)
+ if gotOverride {
+ t.Fatal("override should not be set for invalid mode value")
+ }
+ })
+
+ t.Run("empty header value is ignored", func(t *testing.T) {
+ var gotOverride bool
+ down, up := newTestTimers()
+ s := &Shim{
+ config: config{PlacementURL: "http://unused"},
+ maxBodyLogSize: 4096,
+ downstreamRequestTimer: down,
+ upstreamRequestTimer: up,
+ }
+ wrapped := s.wrapHandler("/test", func(w http.ResponseWriter, r *http.Request) {
+ _, gotOverride = r.Context().Value(featureModeOverrideKey).(FeatureMode)
+ w.WriteHeader(http.StatusOK)
+ })
+ req := httptest.NewRequest(http.MethodGet, "/test", http.NoBody)
+ req.Header.Set(headerFeatureModeOverride, "")
+ w := httptest.NewRecorder()
+ wrapped(w, req)
+ if gotOverride {
+ t.Fatal("override should not be set for empty header")
+ }
+ })
+}
From 3f4e3b8cfd92b6e040aa5abcfdf421103cc6454b Mon Sep 17 00:00:00 2001
From: Philipp Matthes
Date: Tue, 28 Apr 2026 16:03:03 +0200
Subject: [PATCH 2/2] fix: reject mode override when backing config is missing,
improve probe error handling
Address review feedback:
- featureModeFromConfOrHeader now rejects overrides to hybrid/crd when
neither Versioning nor Traits config was validated at startup, preventing
handlers from hitting nil-pointer paths.
- e2eProbeUnimplemented now returns an error for unexpected 4xx/5xx status
codes (other than 501) instead of silently treating them as success.
---
internal/shim/placement/shim.go | 19 ++++++++++++++-----
internal/shim/placement/shim_e2e.go | 7 +++++--
internal/shim/placement/shim_test.go | 28 ++++++++++++++++++++++++++--
3 files changed, 45 insertions(+), 9 deletions(-)
diff --git a/internal/shim/placement/shim.go b/internal/shim/placement/shim.go
index 4e09c56c3..b46546b63 100644
--- a/internal/shim/placement/shim.go
+++ b/internal/shim/placement/shim.go
@@ -116,13 +116,22 @@ func (s *Shim) dispatchPassthroughOnly(w http.ResponseWriter, r *http.Request, m
// featureModeFromConfOrHeader returns the effective feature mode for the
// current request. If a valid override is present in the request context
// (injected by wrapHandler from the X-Cortex-Feature-Mode header), the
-// override takes precedence. Otherwise the configured mode's default is
-// returned.
+// override takes precedence — unless it would escalate from passthrough into
+// a mode that requires backing config (Versioning, Traits) that was not
+// validated at startup. In that case the override is ignored and the
+// configured default is returned.
func (s *Shim) featureModeFromConfOrHeader(r *http.Request, configured FeatureMode) FeatureMode {
- if override, ok := r.Context().Value(featureModeOverrideKey).(FeatureMode); ok {
- return override.orDefault()
+ override, ok := r.Context().Value(featureModeOverrideKey).(FeatureMode)
+ if !ok {
+ return configured.orDefault()
+ }
+ resolved := override.orDefault()
+ if resolved == FeatureModeHybrid || resolved == FeatureModeCRD {
+ if s.config.Versioning == nil && s.config.Traits == nil {
+ return configured.orDefault()
+ }
}
- return configured.orDefault()
+ return resolved
}
// featuresConfig controls the feature mode for each endpoint group.
diff --git a/internal/shim/placement/shim_e2e.go b/internal/shim/placement/shim_e2e.go
index 553912448..d839751a5 100644
--- a/internal/shim/placement/shim_e2e.go
+++ b/internal/shim/placement/shim_e2e.go
@@ -141,8 +141,8 @@ func e2eWrapWithModes(fn func(ctx context.Context, cl client.Client) error) func
// e2eProbeUnimplemented sends a single GET request with the mode override
// header to verify the endpoint returns 501 Not Implemented. Returns true if
// the endpoint is unimplemented for this mode (test should skip). Returns
-// false if the endpoint works (test should continue). Returns an error if
-// something unexpected happened.
+// false if the endpoint returned a success status (test should continue).
+// Returns an error for unexpected status codes (4xx/5xx other than 501).
func e2eProbeUnimplemented(ctx context.Context, sc *gophercloud.ServiceClient, probeURL string) (bool, error) {
log := logf.FromContext(ctx)
mode := e2eCurrentMode(ctx)
@@ -165,6 +165,9 @@ func e2eProbeUnimplemented(ctx context.Context, sc *gophercloud.ServiceClient, p
log.Info("Endpoint correctly returns 501 for unimplemented mode", "mode", mode)
return true, nil
}
+ if resp.StatusCode >= http.StatusBadRequest {
+ return false, fmt.Errorf("probe %s in mode %s returned unexpected status %d", probeURL, mode, resp.StatusCode)
+ }
return false, nil
}
diff --git a/internal/shim/placement/shim_test.go b/internal/shim/placement/shim_test.go
index 2e75e6645..ffc31e954 100644
--- a/internal/shim/placement/shim_test.go
+++ b/internal/shim/placement/shim_test.go
@@ -563,7 +563,9 @@ func TestWrapHandlerWithAuth(t *testing.T) {
}
func TestFeatureModeFromConfOrHeader(t *testing.T) {
- s := &Shim{}
+ s := &Shim{config: config{
+ Traits: &traitsConfig{ConfigMapName: "test"},
+ }}
t.Run("returns configured mode when no override", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
@@ -581,7 +583,7 @@ func TestFeatureModeFromConfOrHeader(t *testing.T) {
}
})
- t.Run("returns override when present in context", func(t *testing.T) {
+ t.Run("returns override when present in context and backing config exists", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureModeCRD)
req = req.WithContext(ctx)
@@ -591,6 +593,28 @@ func TestFeatureModeFromConfOrHeader(t *testing.T) {
}
})
+ t.Run("override to hybrid/crd ignored when no backing config", func(t *testing.T) {
+ bare := &Shim{}
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureModeCRD)
+ req = req.WithContext(ctx)
+ got := bare.featureModeFromConfOrHeader(req, FeatureModePassthrough)
+ if got != FeatureModePassthrough {
+ t.Fatalf("got %q, want %q (override should be rejected without backing config)", got, FeatureModePassthrough)
+ }
+ })
+
+ t.Run("override to passthrough always allowed", func(t *testing.T) {
+ bare := &Shim{}
+ req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
+ ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureModePassthrough)
+ req = req.WithContext(ctx)
+ got := bare.featureModeFromConfOrHeader(req, FeatureModeHybrid)
+ if got != FeatureModePassthrough {
+ t.Fatalf("got %q, want %q", got, FeatureModePassthrough)
+ }
+ })
+
t.Run("override defaults empty to passthrough", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/", http.NoBody)
ctx := context.WithValue(req.Context(), featureModeOverrideKey, FeatureMode(""))