diff --git a/internal/kgateway/extensions2/plugins/backendconfigpolicy/plugin.go b/internal/kgateway/extensions2/plugins/backendconfigpolicy/plugin.go index af7525f5bc4..70406a6077c 100644 --- a/internal/kgateway/extensions2/plugins/backendconfigpolicy/plugin.go +++ b/internal/kgateway/extensions2/plugins/backendconfigpolicy/plugin.go @@ -16,13 +16,16 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/krtcollections" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/logging" sdk "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/collections" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir" + "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter" pluginsdkutils "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/utils" + "github.com/kgateway-dev/kgateway/v2/pkg/reports" "github.com/kgateway-dev/kgateway/v2/pkg/utils/cmputils" "github.com/kgateway-dev/kgateway/v2/pkg/validator" ) @@ -113,16 +116,27 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections, v kclient.Filter{ObjectFilter: commoncol.Client.ObjectFilter()}, ) col := krt.WrapClient(cli, commoncol.KrtOpts.ToOptions("BackendConfigPolicy")...) - backendConfigPolicyCol := krt.NewCollection(col, func(krtctx krt.HandlerContext, b *v1alpha1.BackendConfigPolicy) *ir.PolicyWrapper { + gk := wellknown.BackendConfigPolicyGVK.GroupKind() + + policyStatusMarker, backendConfigPolicyCol := krt.NewStatusCollection(col, func(krtctx krt.HandlerContext, b *v1alpha1.BackendConfigPolicy) (*krtcollections.StatusMarker, *ir.PolicyWrapper) { policyIR, errs := translate(commoncol, krtctx, b) if err := validateXDS(ctx, policyIR, v, commoncol.Settings.ValidationMode); err != nil { errs = append(errs, err) } - return &ir.PolicyWrapper{ + // Create status marker if existing status has kgateway controller + var statusMarker *krtcollections.StatusMarker + for _, ancestor := range b.Status.Ancestors { + if string(ancestor.ControllerName) == commoncol.ControllerName { + statusMarker = &krtcollections.StatusMarker{} + break + } + } + + pol := &ir.PolicyWrapper{ ObjectSource: ir.ObjectSource{ - Group: wellknown.BackendConfigPolicyGVK.Group, - Kind: wellknown.BackendConfigPolicyGVK.Kind, + Group: gk.Group, + Kind: gk.Kind, Namespace: b.Namespace, Name: b.Name, }, @@ -131,15 +145,38 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections, v TargetRefs: pluginsdkutils.TargetRefsToPolicyRefs(b.Spec.TargetRefs, b.Spec.TargetSelectors), Errors: errs, } - }, commoncol.KrtOpts.ToOptions("BackendConfigPolicyIRs")...) + + return statusMarker, pol + }) + + // processMarkers for policies that have existing status but no current report + processMarkers := func(kctx krt.HandlerContext, reportMap *reports.ReportMap) { + objStatus := krt.Fetch(kctx, policyStatusMarker) + for _, status := range objStatus { + policyKey := reporter.PolicyKey{ + Group: gk.Group, + Kind: gk.Kind, + Namespace: status.Obj.GetNamespace(), + Name: status.Obj.GetName(), + } + + // Add empty status to clear stale status for policies with no valid targets + if reportMap.Policies[policyKey] == nil { + rp := reports.NewReporter(reportMap) + // create empty policy report entry with no ancestor refs + rp.Policy(policyKey, 0) + } + } + } return sdk.Plugin{ ContributesPolicies: map[schema.GroupKind]sdk.PolicyPlugin{ wellknown.BackendConfigPolicyGVK.GroupKind(): { - Name: "BackendConfigPolicy", - Policies: backendConfigPolicyCol, - ProcessBackend: processBackend, - GetPolicyStatus: getPolicyStatusFn(cli), - PatchPolicyStatus: patchPolicyStatusFn(cli), + Name: "BackendConfigPolicy", + Policies: backendConfigPolicyCol, + ProcessPolicyStaleStatusMarkers: processMarkers, + ProcessBackend: processBackend, + GetPolicyStatus: getPolicyStatusFn(cli), + PatchPolicyStatus: patchPolicyStatusFn(cli), }, }, } diff --git a/internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go b/internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go index fd9a9e17749..5ebd9f9a277 100644 --- a/internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go +++ b/internal/kgateway/extensions2/plugins/backendtlspolicy/plugin.go @@ -22,12 +22,15 @@ import ( gwv1 "sigs.k8s.io/gateway-api/apis/v1" eiutils "github.com/kgateway-dev/kgateway/v2/internal/envoyinit/pkg/utils" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/krtcollections" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils" kgwellknown "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" sdk "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/collections" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir" + "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter" pluginutils "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/utils" + "github.com/kgateway-dev/kgateway/v2/pkg/reports" ) var ( @@ -73,8 +76,19 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd col := krt.WrapClient(cli, commoncol.KrtOpts.ToOptions("BackendTLSPolicy")...) translate := buildTranslateFunc(commoncol.ConfigMaps) - tlsPolicyCol := krt.NewCollection(col, func(krtctx krt.HandlerContext, i *gwv1.BackendTLSPolicy) *ir.PolicyWrapper { + + policyStatusMarker, tlsPolicyCol := krt.NewStatusCollection(col, func(krtctx krt.HandlerContext, i *gwv1.BackendTLSPolicy) (*krtcollections.StatusMarker, *ir.PolicyWrapper) { tlsPolicyIR, err := translate(krtctx, i) + + // Create status marker if existing status has kgateway controller + var statusMarker *krtcollections.StatusMarker + for _, ancestor := range i.Status.Ancestors { + if string(ancestor.ControllerName) == kgwellknown.DefaultGatewayControllerName { + statusMarker = &krtcollections.StatusMarker{} + break + } + } + pol := &ir.PolicyWrapper{ ObjectSource: ir.ObjectSource{ Group: backendTlsPolicyGroupKind.Group, @@ -89,17 +103,38 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd if err != nil { pol.Errors = []error{err} } - return pol - }, commoncol.KrtOpts.ToOptions("BackendTLSPolicyIRs")...) + return statusMarker, pol + }) + + // processMarkers for policies that have existing status but no current report + processMarkers := func(kctx krt.HandlerContext, reportMap *reports.ReportMap) { + objStatus := krt.Fetch(kctx, policyStatusMarker) + for _, status := range objStatus { + policyKey := reporter.PolicyKey{ + Group: backendTlsPolicyGroupKind.Group, + Kind: backendTlsPolicyGroupKind.Kind, + Namespace: status.Obj.GetNamespace(), + Name: status.Obj.GetName(), + } + + // Add empty status to clear stale status for policies with no valid targets + if reportMap.Policies[policyKey] == nil { + rp := reports.NewReporter(reportMap) + // create empty policy report entry with no ancestor refs + rp.Policy(policyKey, 0) + } + } + } return sdk.Plugin{ ContributesPolicies: map[schema.GroupKind]sdk.PolicyPlugin{ backendTlsPolicyGroupKind.GroupKind(): { - Name: "BackendTLSPolicy", - Policies: tlsPolicyCol, - ProcessBackend: processBackend, - GetPolicyStatus: getPolicyStatusFn(cli), - PatchPolicyStatus: patchPolicyStatusFn(cli), + Name: "BackendTLSPolicy", + Policies: tlsPolicyCol, + ProcessPolicyStaleStatusMarkers: processMarkers, + ProcessBackend: processBackend, + GetPolicyStatus: getPolicyStatusFn(cli), + PatchPolicyStatus: patchPolicyStatusFn(cli), }, }, } diff --git a/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go b/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go index 5cdbfdcb61f..08c8a495b7c 100644 --- a/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go +++ b/internal/kgateway/extensions2/plugins/httplistenerpolicy/httplistener_plugin.go @@ -24,6 +24,7 @@ import ( "k8s.io/utils/ptr" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/krtcollections" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/logging" @@ -34,6 +35,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/policy" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter" pluginsdkutils "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/utils" + "github.com/kgateway-dev/kgateway/v2/pkg/reports" "github.com/kgateway-dev/kgateway/v2/pkg/utils/cmputils" ) @@ -175,7 +177,8 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd ) col := krt.WrapClient(cli, commoncol.KrtOpts.ToOptions("HTTPListenerPolicy")...) gk := wellknown.HTTPListenerPolicyGVK.GroupKind() - policyCol := krt.NewCollection(col, func(krtctx krt.HandlerContext, i *v1alpha1.HTTPListenerPolicy) *ir.PolicyWrapper { + + policyStatusMarker, policyCol := krt.NewStatusCollection(col, func(krtctx krt.HandlerContext, i *v1alpha1.HTTPListenerPolicy) (*krtcollections.StatusMarker, *ir.PolicyWrapper) { objSrc := ir.ObjectSource{ Group: gk.Group, Kind: gk.Kind, @@ -218,6 +221,15 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd xffNumTrustedHops = ptr.To(uint32(*i.Spec.XffNumTrustedHops)) // nolint:gosec // G115: kubebuilder validation ensures safe for uint32 } + // Create status marker if existing status has kgateway controller + var statusMarker *krtcollections.StatusMarker + for _, ancestor := range i.Status.Ancestors { + if string(ancestor.ControllerName) == commoncol.ControllerName { + statusMarker = &krtcollections.StatusMarker{} + break + } + } + pol := &ir.PolicyWrapper{ ObjectSource: objSrc, Policy: i, @@ -242,16 +254,37 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections) sd Errors: errs, } - return pol + return statusMarker, pol }) + // processMarkers for policies that have existing status but no current report + processMarkers := func(kctx krt.HandlerContext, reportMap *reports.ReportMap) { + objStatus := krt.Fetch(kctx, policyStatusMarker) + for _, status := range objStatus { + policyKey := reporter.PolicyKey{ + Group: gk.Group, + Kind: gk.Kind, + Namespace: status.Obj.GetNamespace(), + Name: status.Obj.GetName(), + } + + // Add empty status to clear stale status for policies with no valid targets + if reportMap.Policies[policyKey] == nil { + rp := reports.NewReporter(reportMap) + // create empty policy report entry with no ancestor refs + rp.Policy(policyKey, 0) + } + } + } + return sdk.Plugin{ ContributesPolicies: map[schema.GroupKind]sdk.PolicyPlugin{ wellknown.HTTPListenerPolicyGVK.GroupKind(): { - NewGatewayTranslationPass: NewGatewayTranslationPass, - Policies: policyCol, - GetPolicyStatus: getPolicyStatusFn(cli), - PatchPolicyStatus: patchPolicyStatusFn(cli), + NewGatewayTranslationPass: NewGatewayTranslationPass, + Policies: policyCol, + ProcessPolicyStaleStatusMarkers: processMarkers, + GetPolicyStatus: getPolicyStatusFn(cli), + PatchPolicyStatus: patchPolicyStatusFn(cli), MergePolicies: func(pols []ir.PolicyAtt) ir.PolicyAtt { return policy.MergePolicies(pols, mergePolicies, "" /*no merge settings*/) }, diff --git a/internal/kgateway/extensions2/plugins/trafficpolicy/traffic_policy_plugin.go b/internal/kgateway/extensions2/plugins/trafficpolicy/traffic_policy_plugin.go index 9850e619b12..9bf5c084fa7 100644 --- a/internal/kgateway/extensions2/plugins/trafficpolicy/traffic_policy_plugin.go +++ b/internal/kgateway/extensions2/plugins/trafficpolicy/traffic_policy_plugin.go @@ -24,6 +24,7 @@ import ( apiannotations "github.com/kgateway-dev/kgateway/v2/api/annotations" "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/krtcollections" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/utils" "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/logging" @@ -34,6 +35,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/policy" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter" pluginsdkutils "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/utils" + "github.com/kgateway-dev/kgateway/v2/pkg/reports" "github.com/kgateway-dev/kgateway/v2/pkg/validator" ) @@ -216,7 +218,7 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections, me constructor := NewTrafficPolicyConstructor(ctx, commoncol) // TrafficPolicy IR will have TypedConfig -> implement backendroute method to add prompt guard, etc. - policyCol := krt.NewCollection(col, func(krtctx krt.HandlerContext, policyCR *v1alpha1.TrafficPolicy) *ir.PolicyWrapper { + statusCol, policyCol := krt.NewStatusCollection(col, func(krtctx krt.HandlerContext, policyCR *v1alpha1.TrafficPolicy) (*krtcollections.StatusMarker, *ir.PolicyWrapper) { objSrc := ir.ObjectSource{ Group: gk.Group, Kind: gk.Kind, @@ -234,6 +236,14 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections, me errors = append(errors, err) } + var statusMarker *krtcollections.StatusMarker + for _, ancestor := range policyCR.Status.Ancestors { + if string(ancestor.ControllerName) == commoncol.ControllerName { + statusMarker = &krtcollections.StatusMarker{} + break + } + } + pol := &ir.PolicyWrapper{ ObjectSource: objSrc, Policy: policyCR, @@ -242,14 +252,35 @@ func NewPlugin(ctx context.Context, commoncol *collections.CommonCollections, me Errors: errors, PrecedenceWeight: precedenceWeight, } - return pol + return statusMarker, pol }) + // processMarkers for policies that have existing status but no current report + processMarkers := func(kctx krt.HandlerContext, reportMap *reports.ReportMap) { + objStatus := krt.Fetch(kctx, statusCol) + for _, status := range objStatus { + policyKey := reporter.PolicyKey{ + Group: gk.Group, + Kind: gk.Kind, + Namespace: status.Obj.GetNamespace(), + Name: status.Obj.GetName(), + } + + // Add empty status to clear stale status for policies with no valid targets + if reportMap.Policies[policyKey] == nil { + rp := reports.NewReporter(reportMap) + // create empty policy report entry with no ancestor refs + rp.Policy(policyKey, 0) + } + } + } + return sdk.Plugin{ ContributesPolicies: map[schema.GroupKind]sdk.PolicyPlugin{ wellknown.TrafficPolicyGVK.GroupKind(): { - NewGatewayTranslationPass: NewGatewayTranslationPass, - Policies: policyCol, + NewGatewayTranslationPass: NewGatewayTranslationPass, + Policies: policyCol, + ProcessPolicyStaleStatusMarkers: processMarkers, MergePolicies: func(pols []ir.PolicyAtt) ir.PolicyAtt { return policy.MergePolicies(pols, mergeTrafficPolicies, mergeSettings) }, diff --git a/internal/kgateway/proxy_syncer/proxy_syncer.go b/internal/kgateway/proxy_syncer/proxy_syncer.go index 7e1454f1859..5548d737c71 100644 --- a/internal/kgateway/proxy_syncer/proxy_syncer.go +++ b/internal/kgateway/proxy_syncer/proxy_syncer.go @@ -257,6 +257,13 @@ func (s *ProxySyncer) Init(ctx context.Context, krtopts krtutil.KrtOptions) { s.backendPolicyReport = krt.NewSingleton(func(kctx krt.HandlerContext) *report { backends := krt.Fetch(kctx, finalBackendsWithPolicyStatus) merged := GenerateBackendPolicyReport(backends) + + for _, plugin := range s.plugins.ContributesPolicies { + if plugin.ProcessPolicyStaleStatusMarkers != nil && plugin.ProcessBackend != nil { + plugin.ProcessPolicyStaleStatusMarkers(kctx, &merged) + } + } + return &report{merged} }, krtopts.ToOptions("BackendsPolicyReport")...) @@ -271,6 +278,12 @@ func (s *ProxySyncer) Init(ctx context.Context, krtopts krtutil.KrtOptions) { objStatus := krt.Fetch(kctx, s.commonCols.Routes.GetHTTPRouteStatusMarkers()) s.commonCols.Routes.ProcessHTTPRouteStatusMarkers(objStatus, merged) + for _, plugin := range s.plugins.ContributesPolicies { + if plugin.ProcessPolicyStaleStatusMarkers != nil && plugin.ProcessBackend == nil { + plugin.ProcessPolicyStaleStatusMarkers(kctx, &merged) + } + } + return &report{merged} }) diff --git a/pkg/pluginsdk/types.go b/pkg/pluginsdk/types.go index 2a51f3b3eb7..b00e9c40fff 100644 --- a/pkg/pluginsdk/types.go +++ b/pkg/pluginsdk/types.go @@ -17,6 +17,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/internal/kgateway/endpoints" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/ir" "github.com/kgateway-dev/kgateway/v2/pkg/pluginsdk/reporter" + "github.com/kgateway-dev/kgateway/v2/pkg/reports" ) // ErrNotFound is returned when a requested resource is not found @@ -64,8 +65,10 @@ type PolicyPlugin struct { // Backend processing for agent gateway ProcessAgentBackend func(pol ir.PolicyIR, in ir.BackendObjectIR) error - Policies krt.Collection[ir.PolicyWrapper] - GlobalPolicies func(krt.HandlerContext) ir.PolicyIR + Policies krt.Collection[ir.PolicyWrapper] + // ProcessPolicyStaleStatusMarkers add empty reports for policies to clear stale status + ProcessPolicyStaleStatusMarkers func(krt.HandlerContext, *reports.ReportMap) + GlobalPolicies func(krt.HandlerContext) ir.PolicyIR // PoliciesFetch can optionally be set if the plugin needs a custom mechanism for fetching the policy IR, // rather than the default behavior of fetching by name from the aggregated policy KRT collection PoliciesFetch func(n, ns string) ir.PolicyIR diff --git a/pkg/reports/policy.go b/pkg/reports/policy.go index cdffff051e1..974e430a4c2 100644 --- a/pkg/reports/policy.go +++ b/pkg/reports/policy.go @@ -112,7 +112,7 @@ func (r *ReportMap) BuildPolicyStatus( } ancestorRefs := report.ancestorRefs() - status := gwv1.PolicyStatus{} + status := gwv1.PolicyStatus{Ancestors: make([]gwv1.PolicyAncestorStatus, 0, len(ancestorRefs))} // Process the parent references to build the RouteParentStatus for _, ancestorRef := range ancestorRefs { diff --git a/pkg/reports/policy_test.go b/pkg/reports/policy_test.go index 0077543bb24..a2ff1a3ecdd 100644 --- a/pkg/reports/policy_test.go +++ b/pkg/reports/policy_test.go @@ -227,6 +227,51 @@ func TestPolicyStatusReport(t *testing.T) { }, }, }, + { + name: "status on existing object and report map with empty policy entry during translation", + fakeTranslation: func(a *assert.Assertions, statusReporter reporter.Reporter) { + // Policy is added to report map but no ancestor refs are added + policyReport := statusReporter.Policy(reporter.PolicyKey{ + Group: "example.com", + Kind: "Policy", + Namespace: "default", + Name: "example", + }, 2) + a.NotNil(policyReport) + }, + key: reporter.PolicyKey{ + Group: "example.com", + Kind: "Policy", + Namespace: "default", + Name: "example", + }, + controller: "example-controller", + currentStatus: gwv1.PolicyStatus{ + Ancestors: []gwv1.PolicyAncestorStatus{ + // Existing stale status for gw-1 that should be cleared + { + AncestorRef: gwv1.ParentReference{ + Group: ptr.To(gwv1.Group("gateway.networking.k8s.io")), + Kind: ptr.To(gwv1.Kind("Gateway")), + Namespace: ptr.To(gwv1.Namespace("default")), + Name: gwv1.ObjectName("gw-1"), + }, + ControllerName: "example-controller", + Conditions: []metav1.Condition{ + { + ObservedGeneration: 1, + Type: string(v1alpha1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1alpha1.PolicyReasonValid), + }, + }, + }, + }, + }, + wantStatus: &gwv1.PolicyStatus{ + Ancestors: []gwv1.PolicyAncestorStatus{}, + }, + }, { name: "preserve ancestor status belonging to external controllers", fakeTranslation: func(a *assert.Assertions, statusReporter reporter.Reporter) { diff --git a/test/e2e/features/backendconfigpolicy/suite.go b/test/e2e/features/backendconfigpolicy/suite.go index 2aed6968dbc..9336ffa7e33 100644 --- a/test/e2e/features/backendconfigpolicy/suite.go +++ b/test/e2e/features/backendconfigpolicy/suite.go @@ -16,13 +16,17 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/anypb" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" "github.com/kgateway-dev/kgateway/v2/pkg/utils/requestutils/curl" "github.com/kgateway-dev/kgateway/v2/test/e2e" testdefaults "github.com/kgateway-dev/kgateway/v2/test/e2e/defaults" "github.com/kgateway-dev/kgateway/v2/test/envoyutils/admincli" testmatchers "github.com/kgateway-dev/kgateway/v2/test/gomega/matchers" + "github.com/kgateway-dev/kgateway/v2/test/helpers" "github.com/kgateway-dev/kgateway/v2/test/testutils" ) @@ -76,6 +80,10 @@ func (s *testingSuite) SetupSuite() { setupManifest, outlierDetectionManifest, }, + "TestBackendConfigPolicyClearStaleStatus": { + testdefaults.CurlPodManifest, + setupManifest, + }, } } @@ -336,3 +344,100 @@ func (s *testingSuite) TestBackendConfigPolicyOutlierDetection() { }).WithTimeout(20 * time.Second).WithPolling(time.Second).Should(gomega.Succeed()) }) } + +const ( + kgatewayControllerName = "kgateway.dev/kgateway" + otherControllerName = "other-controller.example.com/controller" +) + +// TestBackendConfigPolicyClearStaleStatus verifies that stale status is cleared when targetRef becomes invalid +func (s *testingSuite) TestBackendConfigPolicyClearStaleStatus() { + // Test applies setup.yaml via BeforeTest which includes "example-policy" targeting Service "example-svc" + // Add fake ancestor status from another controller + s.addAncestorStatus("example-policy", "default", otherControllerName) + + // Verify both kgateway and other controller statuses exist + s.assertAncestorStatuses("example-svc", map[string]bool{ + kgatewayControllerName: true, + otherControllerName: true, + }) + + // Apply policy with missing service target + err := s.testInstallation.Actions.Kubectl().ApplyFile( + s.ctx, + missingTargetManifest, + ) + s.Require().NoError(err) + + // Verify kgateway status cleared, other remains + s.assertAncestorStatuses("example-svc", map[string]bool{ + kgatewayControllerName: false, + otherControllerName: true, + }) + // AfterTest() handles cleanup automatically +} + +func (s *testingSuite) addAncestorStatus(policyName, policyNamespace, controllerName string) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.testInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.BackendConfigPolicy{} + err := s.testInstallation.ClusterContext.Client.Get( + s.ctx, + types.NamespacedName{Name: policyName, Namespace: policyNamespace}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + // Add fake ancestor status + fakeStatus := gwv1.PolicyAncestorStatus{ + AncestorRef: gwv1.ParentReference{ + Group: func() *gwv1.Group { g := gwv1.Group(""); return &g }(), + Kind: func() *gwv1.Kind { k := gwv1.Kind("Service"); return &k }(), + Name: "example-svc", + }, + ControllerName: gwv1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(v1alpha1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1alpha1.PolicyReasonValid), + Message: "Accepted by fake controller", + LastTransitionTime: metav1.Now(), + }, + }, + } + + policy.Status.Ancestors = append(policy.Status.Ancestors, fakeStatus) + err = s.testInstallation.ClusterContext.Client.Status().Update(s.ctx, policy) + g.Expect(err).NotTo(gomega.HaveOccurred()) + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} + +func (s *testingSuite) assertAncestorStatuses(ancestorName string, expectedControllers map[string]bool) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.testInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.BackendConfigPolicy{} + err := s.testInstallation.ClusterContext.Client.Get( + s.ctx, + types.NamespacedName{Name: "example-policy", Namespace: "default"}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + foundControllers := make(map[string]bool) + for _, ancestor := range policy.Status.Ancestors { + if string(ancestor.AncestorRef.Name) == ancestorName { + foundControllers[string(ancestor.ControllerName)] = true + } + } + + for controller, shouldExist := range expectedControllers { + exists := foundControllers[controller] + if shouldExist { + g.Expect(exists).To(gomega.BeTrue(), "Expected controller %s to exist in status", controller) + } else { + g.Expect(exists).To(gomega.BeFalse(), "Expected controller %s to not exist in status", controller) + } + } + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} diff --git a/test/e2e/features/backendconfigpolicy/testdata/missing-target.yaml b/test/e2e/features/backendconfigpolicy/testdata/missing-target.yaml new file mode 100644 index 00000000000..520743de4fc --- /dev/null +++ b/test/e2e/features/backendconfigpolicy/testdata/missing-target.yaml @@ -0,0 +1,10 @@ +kind: BackendConfigPolicy +apiVersion: gateway.kgateway.dev/v1alpha1 +metadata: + name: example-policy +spec: + targetRefs: + - name: missing-svc + group: "" + kind: Service + connectTimeout: 5s diff --git a/test/e2e/features/backendconfigpolicy/types.go b/test/e2e/features/backendconfigpolicy/types.go index f586cdba362..67fb64488f9 100644 --- a/test/e2e/features/backendconfigpolicy/types.go +++ b/test/e2e/features/backendconfigpolicy/types.go @@ -20,6 +20,7 @@ var ( simpleTLSManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "simple-tls.yaml") systemCAManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "system-ca.yaml") outlierDetectionManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "outlierdetection.yaml") + missingTargetManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "missing-target.yaml") // objects proxyObjectMeta = metav1.ObjectMeta{ Name: "gw", diff --git a/test/e2e/features/backendtls/suite.go b/test/e2e/features/backendtls/suite.go index ed3f04aa85d..c6173110140 100644 --- a/test/e2e/features/backendtls/suite.go +++ b/test/e2e/features/backendtls/suite.go @@ -13,6 +13,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" gwv1 "sigs.k8s.io/gateway-api/apis/v1" @@ -31,7 +32,8 @@ import ( ) var ( - configMapManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata/configmap.yaml") + configMapManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata/configmap.yaml") + backendTLSPolicyMissingTargetManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata/missing-target.yaml") proxyObjMeta = metav1.ObjectMeta{ Name: "gw", @@ -218,3 +220,106 @@ func (s *tsuite) assertPolicyStatus(inCondition metav1.Condition) { } }, currentTimeout, pollingInterval).Should(gomega.Succeed()) } + +const ( + kgatewayControllerName = "kgateway.dev/kgateway" + otherControllerName = "other-controller.example.com/controller" +) + +// TestBackendTLSPolicyClearStaleStatus verifies that stale status is cleared when targetRef becomes invalid +func (s *tsuite) TestBackendTLSPolicyClearStaleStatus() { + if s.agentgateway { + s.T().Log("Skipping status test for Agentgateway as statuses are not currently supported") + return + } + + // Test applies base.yaml via setup which includes "tls-policy" targeting Services "nginx" and "nginx2" + // Add fake ancestor status from another controller + s.addAncestorStatus("tls-policy", "default", otherControllerName) + + // Verify both kgateway and other controller statuses exist + s.assertAncestorStatuses("nginx", map[string]bool{ + kgatewayControllerName: true, + otherControllerName: true, + }) + + // Apply policy with missing service target + err := s.TestInstallation.Actions.Kubectl().ApplyFile( + s.Ctx, + backendTLSPolicyMissingTargetManifest, + ) + s.Require().NoError(err) + + // Verify kgateway status cleared, other remains + s.assertAncestorStatuses("nginx", map[string]bool{ + kgatewayControllerName: false, + otherControllerName: true, + }) + // AfterTest() handles cleanup automatically +} + +func (s *tsuite) addAncestorStatus(policyName, policyNamespace, controllerName string) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.TestInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &gwv1.BackendTLSPolicy{} + err := s.TestInstallation.ClusterContext.Client.Get( + s.Ctx, + types.NamespacedName{Name: policyName, Namespace: policyNamespace}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + // Add fake ancestor status + fakeStatus := gwv1.PolicyAncestorStatus{ + AncestorRef: gwv1.ParentReference{ + Group: (*gwv1.Group)(&svcGroup), + Kind: (*gwv1.Kind)(&svcKind), + Namespace: ptr.To(gwv1.Namespace(nginxMeta.Namespace)), + Name: gwv1.ObjectName(nginxMeta.Name), + }, + ControllerName: gwv1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(v1alpha1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1alpha1.PolicyReasonValid), + Message: "Accepted by fake controller", + LastTransitionTime: metav1.Now(), + }, + }, + } + + policy.Status.Ancestors = append(policy.Status.Ancestors, fakeStatus) + err = s.TestInstallation.ClusterContext.Client.Status().Update(s.Ctx, policy) + g.Expect(err).NotTo(gomega.HaveOccurred()) + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} + +func (s *tsuite) assertAncestorStatuses(ancestorName string, expectedControllers map[string]bool) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.TestInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &gwv1.BackendTLSPolicy{} + err := s.TestInstallation.ClusterContext.Client.Get( + s.Ctx, + types.NamespacedName{Name: "tls-policy", Namespace: "default"}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + foundControllers := make(map[string]bool) + for _, ancestor := range policy.Status.Ancestors { + if string(ancestor.AncestorRef.Name) == ancestorName { + foundControllers[string(ancestor.ControllerName)] = true + } + } + + for controller, shouldExist := range expectedControllers { + exists := foundControllers[controller] + if shouldExist { + g.Expect(exists).To(gomega.BeTrue(), "Expected controller %s to exist in status", controller) + } else { + g.Expect(exists).To(gomega.BeFalse(), "Expected controller %s to not exist in status", controller) + } + } + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} diff --git a/test/e2e/features/backendtls/testdata/missing-target.yaml b/test/e2e/features/backendtls/testdata/missing-target.yaml new file mode 100644 index 00000000000..d5bb75bc724 --- /dev/null +++ b/test/e2e/features/backendtls/testdata/missing-target.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: BackendTLSPolicy +metadata: + name: tls-policy +spec: + targetRefs: + - group: "" + kind: Service + name: missing-nginx + validation: + hostname: "example.com" + wellKnownCACertificates: System diff --git a/test/e2e/features/http_listener_policy/suite.go b/test/e2e/features/http_listener_policy/suite.go index d0591577565..69324548371 100644 --- a/test/e2e/features/http_listener_policy/suite.go +++ b/test/e2e/features/http_listener_policy/suite.go @@ -12,12 +12,17 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" "github.com/kgateway-dev/kgateway/v2/pkg/utils/kubeutils" "github.com/kgateway-dev/kgateway/v2/pkg/utils/requestutils/curl" "github.com/kgateway-dev/kgateway/v2/test/e2e" testdefaults "github.com/kgateway-dev/kgateway/v2/test/e2e/defaults" "github.com/kgateway-dev/kgateway/v2/test/gomega/matchers" + "github.com/kgateway-dev/kgateway/v2/test/helpers" "github.com/kgateway-dev/kgateway/v2/test/testutils" ) @@ -54,10 +59,11 @@ func (s *testingSuite) SetupSuite() { // include gateway manifests for the tests, so we recreate it for each test run s.manifests = map[string][]string{ - "TestHttpListenerPolicyAllFields": {gatewayManifest, httpRouteManifest, allFieldsManifest}, - "TestHttpListenerPolicyServerHeader": {gatewayManifest, httpRouteManifest, serverHeaderManifest}, - "TestPreserveHttp1HeaderCase": {gatewayManifest, preserveHttp1HeaderCaseManifest}, - "TestAccessLogEmittedToStdout": {gatewayManifest, httpRouteManifest, accessLogManifest}, + "TestHttpListenerPolicyAllFields": {gatewayManifest, httpRouteManifest, allFieldsManifest}, + "TestHttpListenerPolicyServerHeader": {gatewayManifest, httpRouteManifest, serverHeaderManifest}, + "TestPreserveHttp1HeaderCase": {gatewayManifest, preserveHttp1HeaderCaseManifest}, + "TestAccessLogEmittedToStdout": {gatewayManifest, httpRouteManifest, accessLogManifest}, + "TestHttpListenerPolicyClearStaleStatus": {gatewayManifest, httpRouteManifest, serverHeaderManifest}, } } @@ -229,3 +235,96 @@ func (s *testingSuite) TestAccessLogEmittedToStdout() { return out }, 10*time.Second, 200*time.Millisecond).ShouldNot(gomega.ContainSubstring("\"response_code\":200")) } + +// TestHttpListenerPolicyClearStaleStatus verifies that stale status is cleared when targetRef becomes invalid +func (s *testingSuite) TestHttpListenerPolicyClearStaleStatus() { + kgatewayControllerName := wellknown.DefaultGatewayControllerName + otherControllerName := "other-controller.example.com/controller" + + // Add fake ancestor status from another controller + s.addAncestorStatus("http-listener-policy-server-header", "default", "other-gw", otherControllerName) + + // Verify both kgateway and other controller statuses exist + s.assertAncestorStatuses("gw", map[string]bool{ + kgatewayControllerName: true, + }) + s.assertAncestorStatuses("other-gw", map[string]bool{ + otherControllerName: true, + }) + + // Apply policy with missing gateway target + err := s.testInstallation.Actions.Kubectl().ApplyFile( + s.ctx, + httpListenerPolicyMissingTargetManifest, + ) + s.Require().NoError(err) + + // Verify kgateway status cleared, other remains + s.assertAncestorStatuses("gw", map[string]bool{ + kgatewayControllerName: false, + }) + s.assertAncestorStatuses("other-gw", map[string]bool{ + otherControllerName: true, + }) +} + +func (s *testingSuite) addAncestorStatus(policyName, policyNamespace, gwName, controllerName string) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.testInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.HTTPListenerPolicy{} + err := s.testInstallation.ClusterContext.Client.Get( + s.ctx, + types.NamespacedName{Name: policyName, Namespace: policyNamespace}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + // Add fake ancestor status + fakeStatus := gwv1.PolicyAncestorStatus{ + AncestorRef: gwv1.ParentReference{Name: gwv1.ObjectName(gwName)}, + ControllerName: gwv1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(v1alpha1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1alpha1.PolicyReasonValid), + Message: "Accepted by fake controller", + LastTransitionTime: metav1.Now(), + }, + }, + } + + policy.Status.Ancestors = append(policy.Status.Ancestors, fakeStatus) + err = s.testInstallation.ClusterContext.Client.Status().Update(s.ctx, policy) + g.Expect(err).NotTo(gomega.HaveOccurred()) + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} + +func (s *testingSuite) assertAncestorStatuses(ancestorName string, expectedControllers map[string]bool) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.testInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.HTTPListenerPolicy{} + err := s.testInstallation.ClusterContext.Client.Get( + s.ctx, + types.NamespacedName{Name: "http-listener-policy-server-header", Namespace: "default"}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + foundControllers := make(map[string]bool) + for _, ancestor := range policy.Status.Ancestors { + if string(ancestor.AncestorRef.Name) == ancestorName { + foundControllers[string(ancestor.ControllerName)] = true + } + } + + for controller, shouldExist := range expectedControllers { + exists := foundControllers[controller] + if shouldExist { + g.Expect(exists).To(gomega.BeTrue(), "Expected controller %s to exist in status", controller) + } else { + g.Expect(exists).To(gomega.BeFalse(), "Expected controller %s to not exist in status", controller) + } + } + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} diff --git a/test/e2e/features/http_listener_policy/testdata/http-listener-policy-missing-target.yaml b/test/e2e/features/http_listener_policy/testdata/http-listener-policy-missing-target.yaml new file mode 100644 index 00000000000..a323bafa12c --- /dev/null +++ b/test/e2e/features/http_listener_policy/testdata/http-listener-policy-missing-target.yaml @@ -0,0 +1,12 @@ +apiVersion: gateway.kgateway.dev/v1alpha1 +kind: HTTPListenerPolicy +metadata: + name: http-listener-policy-server-header + namespace: default +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: missing-gw + serverHeaderTransformation: PassThrough + diff --git a/test/e2e/features/http_listener_policy/types.go b/test/e2e/features/http_listener_policy/types.go index 2c0b9b7716c..886afdefa63 100644 --- a/test/e2e/features/http_listener_policy/types.go +++ b/test/e2e/features/http_listener_policy/types.go @@ -13,13 +13,14 @@ import ( ) var ( - setupManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "setup.yaml") - gatewayManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "gateway.yaml") - httpRouteManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "httproute.yaml") - allFieldsManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-all-fields.yaml") - serverHeaderManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-server-header.yaml") - preserveHttp1HeaderCaseManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "preserve-http1-header-case.yaml") - accessLogManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-access-log.yaml") + setupManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "setup.yaml") + gatewayManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "gateway.yaml") + httpRouteManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "httproute.yaml") + allFieldsManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-all-fields.yaml") + serverHeaderManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-server-header.yaml") + preserveHttp1HeaderCaseManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "preserve-http1-header-case.yaml") + accessLogManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-access-log.yaml") + httpListenerPolicyMissingTargetManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "http-listener-policy-missing-target.yaml") // When we apply the setup file, we expect resources to be created with this metadata proxyObjectMeta = metav1.ObjectMeta{ diff --git a/test/e2e/features/trafficpolicystatus/suite.go b/test/e2e/features/trafficpolicystatus/suite.go new file mode 100644 index 00000000000..bc091bf4f02 --- /dev/null +++ b/test/e2e/features/trafficpolicystatus/suite.go @@ -0,0 +1,125 @@ +//go:build e2e + +package trafficpolicystatus + +import ( + "context" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/suite" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + + "github.com/kgateway-dev/kgateway/v2/api/v1alpha1" + "github.com/kgateway-dev/kgateway/v2/internal/kgateway/wellknown" + "github.com/kgateway-dev/kgateway/v2/test/e2e" + "github.com/kgateway-dev/kgateway/v2/test/e2e/tests/base" + "github.com/kgateway-dev/kgateway/v2/test/helpers" +) + +var _ e2e.NewSuiteFunc = NewTestingSuite + +// testingSuite is a suite of tests for testing TrafficPolicy status behavior +type testingSuite struct { + *base.BaseTestingSuite +} + +func NewTestingSuite(ctx context.Context, testInst *e2e.TestInstallation) suite.TestingSuite { + return &testingSuite{ + BaseTestingSuite: base.NewBaseTestingSuite(ctx, testInst, setup, testCases), + } +} + +// TestTrafficPolicyClearStaleStatus verifies that stale status is cleared when targetRef becomes invalid +func (s *testingSuite) TestTrafficPolicyClearStaleStatus() { + kgatewayControllerName := wellknown.DefaultGatewayControllerName + otherControllerName := "other-controller.example.com/controller" + + // Add fake ancestor status from another controller + s.addAncestorStatus("example-policy", "default", "other-gw", otherControllerName) + + // Verify both kgateway and other controller statuses exist + s.assertAncestorStatuses("gw", map[string]bool{ + kgatewayControllerName: true, + }) + s.assertAncestorStatuses("other-gw", map[string]bool{ + otherControllerName: true, + }) + + // Apply policy with missing gateway target + err := s.TestInstallation.Actions.Kubectl().ApplyFile( + s.Ctx, + policyWithMissingGwManifest, + ) + s.Require().NoError(err) + + // Verify kgateway status cleared, other remains + s.assertAncestorStatuses("gw", map[string]bool{ + kgatewayControllerName: false, + }) + s.assertAncestorStatuses("other-gw", map[string]bool{ + otherControllerName: true, + }) +} + +func (s *testingSuite) addAncestorStatus(policyName, policyNamespace, gwName, controllerName string) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.TestInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.TrafficPolicy{} + err := s.TestInstallation.ClusterContext.Client.Get( + s.Ctx, + types.NamespacedName{Name: policyName, Namespace: policyNamespace}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + // Add fake ancestor status + fakeStatus := gwv1.PolicyAncestorStatus{ + AncestorRef: gwv1.ParentReference{Name: gwv1.ObjectName(gwName)}, + ControllerName: gwv1.GatewayController(controllerName), + Conditions: []metav1.Condition{ + { + Type: string(v1alpha1.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(v1alpha1.PolicyReasonValid), + Message: "Accepted by fake controller", + LastTransitionTime: metav1.Now(), + }, + }, + } + + policy.Status.Ancestors = append(policy.Status.Ancestors, fakeStatus) + err = s.TestInstallation.ClusterContext.Client.Status().Update(s.Ctx, policy) + g.Expect(err).NotTo(gomega.HaveOccurred()) + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} + +func (s *testingSuite) assertAncestorStatuses(ancestorName string, expectedControllers map[string]bool) { + currentTimeout, pollingInterval := helpers.GetTimeouts() + s.TestInstallation.Assertions.Gomega.Eventually(func(g gomega.Gomega) { + policy := &v1alpha1.TrafficPolicy{} + err := s.TestInstallation.ClusterContext.Client.Get( + s.Ctx, + types.NamespacedName{Name: "example-policy", Namespace: "default"}, + policy, + ) + g.Expect(err).NotTo(gomega.HaveOccurred()) + + foundControllers := make(map[string]bool) + for _, ancestor := range policy.Status.Ancestors { + if string(ancestor.AncestorRef.Name) == ancestorName { + foundControllers[string(ancestor.ControllerName)] = true + } + } + + for controller, shouldExist := range expectedControllers { + exists := foundControllers[controller] + if shouldExist { + g.Expect(exists).To(gomega.BeTrue(), "Expected controller %s to exist in status", controller) + } else { + g.Expect(exists).To(gomega.BeFalse(), "Expected controller %s to not exist in status", controller) + } + } + }, currentTimeout, pollingInterval).Should(gomega.Succeed()) +} diff --git a/test/e2e/features/trafficpolicystatus/testdata/policy-with-gw.yaml b/test/e2e/features/trafficpolicystatus/testdata/policy-with-gw.yaml new file mode 100644 index 00000000000..dd49284f1d6 --- /dev/null +++ b/test/e2e/features/trafficpolicystatus/testdata/policy-with-gw.yaml @@ -0,0 +1,27 @@ +kind: Gateway +apiVersion: gateway.networking.k8s.io/v1 +metadata: + name: gw +spec: + gatewayClassName: kgateway + listeners: + - protocol: HTTP + port: 8080 + name: http + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.kgateway.dev/v1alpha1 +kind: TrafficPolicy +metadata: + name: example-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: gw + cors: + allowOrigins: + - "https://ignored.com" + diff --git a/test/e2e/features/trafficpolicystatus/testdata/policy-with-missing-gw.yaml b/test/e2e/features/trafficpolicystatus/testdata/policy-with-missing-gw.yaml new file mode 100644 index 00000000000..6979f87f0e0 --- /dev/null +++ b/test/e2e/features/trafficpolicystatus/testdata/policy-with-missing-gw.yaml @@ -0,0 +1,13 @@ +apiVersion: gateway.kgateway.dev/v1alpha1 +kind: TrafficPolicy +metadata: + name: example-policy +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: missing-gw + cors: + allowOrigins: + - "https://ignored.com" + diff --git a/test/e2e/features/trafficpolicystatus/types.go b/test/e2e/features/trafficpolicystatus/types.go new file mode 100644 index 00000000000..7c94783f52b --- /dev/null +++ b/test/e2e/features/trafficpolicystatus/types.go @@ -0,0 +1,24 @@ +//go:build e2e + +package trafficpolicystatus + +import ( + "path/filepath" + + "github.com/kgateway-dev/kgateway/v2/pkg/utils/fsutils" + "github.com/kgateway-dev/kgateway/v2/test/e2e/tests/base" +) + +var ( + // manifests + policyWithGwManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "policy-with-gw.yaml") + policyWithMissingGwManifest = filepath.Join(fsutils.MustGetThisDir(), "testdata", "policy-with-missing-gw.yaml") + + setup = base.TestCase{ + Manifests: []string{policyWithGwManifest}, + } + + testCases = map[string]*base.TestCase{ + "TestTrafficPolicyClearStaleStatus": {}, + } +) diff --git a/test/e2e/tests/kgateway_tests.go b/test/e2e/tests/kgateway_tests.go index 23c4395fb53..f7b84c2cb00 100644 --- a/test/e2e/tests/kgateway_tests.go +++ b/test/e2e/tests/kgateway_tests.go @@ -37,6 +37,7 @@ import ( "github.com/kgateway-dev/kgateway/v2/test/e2e/features/session_persistence" "github.com/kgateway-dev/kgateway/v2/test/e2e/features/timeoutretry" "github.com/kgateway-dev/kgateway/v2/test/e2e/features/tracing" + "github.com/kgateway-dev/kgateway/v2/test/e2e/features/trafficpolicystatus" "github.com/kgateway-dev/kgateway/v2/test/e2e/features/transformation" ) @@ -62,6 +63,7 @@ func KubeGatewaySuiteRunner() e2e.SuiteRunner { kubeGatewaySuiteRunner.Register("LocalRateLimit", local_rate_limit.NewTestingSuite) kubeGatewaySuiteRunner.Register("GlobalRateLimit", global_rate_limit.NewTestingSuite) kubeGatewaySuiteRunner.Register("PolicySelector", policyselector.NewTestingSuite) + kubeGatewaySuiteRunner.Register("TrafficPolicyStatus", trafficpolicystatus.NewTestingSuite) kubeGatewaySuiteRunner.Register("Cors", cors.NewTestingSuite) kubeGatewaySuiteRunner.Register("BackendConfigPolicy", backendconfigpolicy.NewTestingSuite) kubeGatewaySuiteRunner.Register("CSRF", csrf.NewTestingSuite)