diff --git a/CHANGELOG.md b/CHANGELOG.md index f12c2a1a5d..394d42919d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -131,9 +131,10 @@ Adding a new version? You'll need three changes: enforce `KongUpstreamPolicy` status. The controller will set an ancestor status in `KongUpstreamPolicy` status for each of its ancestors (i.e. `Service` or `KongServiceFacade`) with the `Accepted` - condition. + and `Programmed` condition. [#5185](https://github.com/Kong/kubernetes-ingress-controller/pull/5185) [#5428](https://github.com/Kong/kubernetes-ingress-controller/pull/5428) + [#5444](https://github.com/Kong/kubernetes-ingress-controller/pull/5444) - New CRD `KongVault` to reperesent a custom Kong vault for storing senstive data used in plugin configurations. Now users can create a `KongVault` to create a custom Kong vault and reference the values in configurations of diff --git a/internal/controllers/configuration/kongupstreampolicy_controller.go b/internal/controllers/configuration/kongupstreampolicy_controller.go index 8aca948ffd..faf5d2af9d 100644 --- a/internal/controllers/configuration/kongupstreampolicy_controller.go +++ b/internal/controllers/configuration/kongupstreampolicy_controller.go @@ -6,9 +6,11 @@ import ( "time" "github.com/go-logr/logr" + "github.com/kong/kubernetes-ingress-controller/v3/internal/util/kubernetes/object/status" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" k8stypes "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -37,6 +39,7 @@ type KongUpstreamPolicyReconciler struct { Scheme *runtime.Scheme DataplaneClient controllers.DataPlane CacheSyncTimeout time.Duration + StatusQueue *status.Queue // KongServiceFacadeEnabled determines whether the controller should populate the KongUpstreamPolicy's ancestor // status for KongServiceFacades. @@ -68,6 +71,15 @@ func (r *KongUpstreamPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error return err } + // Watch for HTTPRoute changes to trigger reconciliation for the KongUpstreamPolicies referenced by the Services + // of the HTTPRoute. + if err := c.Watch( + source.Kind(mgr.GetCache(), &gatewayapi.HTTPRoute{}), + handler.EnqueueRequestsFromMapFunc(r.getUpstreamPoliciesForHTTPRouteServices), + ); err != nil { + return err + } + if r.KongServiceFacadeEnabled { if err := c.Watch( source.Kind(mgr.GetCache(), &incubatorv1alpha1.KongServiceFacade{}), @@ -78,10 +90,40 @@ func (r *KongUpstreamPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error } } - return c.Watch( + if err := c.Watch( source.Kind(mgr.GetCache(), &kongv1beta1.KongUpstreamPolicy{}), &handler.EnqueueRequestForObject{}, - ) + ); err != nil { + return err + } + + if r.StatusQueue != nil { + // Watch for notifications on the status queue from Services and KongServiceFacades as their status change + // needs to be propagated to the KongUpstreamPolicy's ancestor Programmed status. + if err := c.Watch( + &source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Version: "v1", + Kind: "Service", + })}, + handler.EnqueueRequestsFromMapFunc(r.getUpstreamPolicyForObject), + predicate.NewPredicateFuncs(doesObjectReferUpstreamPolicy), + ); err != nil { + return err + } + if err := c.Watch( + &source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Version: incubatorv1alpha1.SchemeGroupVersion.Version, + Group: incubatorv1alpha1.SchemeGroupVersion.Group, + Kind: incubatorv1alpha1.KongServiceFacadeKind, + })}, + handler.EnqueueRequestsFromMapFunc(r.getUpstreamPolicyForObject), + predicate.NewPredicateFuncs(doesObjectReferUpstreamPolicy), + ); err != nil { + return err + } + } + + return nil } func (r *KongUpstreamPolicyReconciler) setupIndices(mgr ctrl.Manager) error { @@ -212,6 +254,56 @@ func (r *KongUpstreamPolicyReconciler) getUpstreamPolicyForObject(ctx context.Co } } +// getUpstreamPoliciesForHTTPRouteServices enqueues a new reconcile request for the KongUpstreamPolicies referenced by +// the Services of an HTTPRoute. +func (r *KongUpstreamPolicyReconciler) getUpstreamPoliciesForHTTPRouteServices(ctx context.Context, obj client.Object) []reconcile.Request { + httpRoute, ok := obj.(*gatewayapi.HTTPRoute) + if !ok { + return nil + } + + var requests []reconcile.Request + for _, rule := range httpRoute.Spec.Rules { + for _, br := range rule.BackendRefs { + if !isSupportedHTTPRouteBackendRef(br.BackendRef) { + continue + } + + namespace := httpRoute.Namespace + if br.BackendRef.Namespace != nil { + namespace = string(*br.BackendRef.Namespace) + } + service := &corev1.Service{} + if err := r.Client.Get(ctx, k8stypes.NamespacedName{ + Namespace: namespace, + Name: string(br.BackendRef.Name), + }, service); err != nil { + if !apierrors.IsNotFound(err) { + r.Log.Error(err, "Failed to retrieve Service in watch predicates", + "Service", fmt.Sprintf("%s/%s", namespace, string(br.BackendRef.Name)), + ) + } + continue + } + + if service.Annotations == nil { + continue + } + upstreamPolicy, ok := service.Annotations[kongv1beta1.KongUpstreamPolicyAnnotationKey] + if !ok { + continue + } + requests = append(requests, reconcile.Request{ + NamespacedName: k8stypes.NamespacedName{ + Namespace: httpRoute.Namespace, + Name: upstreamPolicy, + }, + }) + } + } + return requests +} + // doesObjectReferUpstreamPolicy filters out all the objects not referencing KongUpstreamPolicies. func doesObjectReferUpstreamPolicy(obj client.Object) bool { annotations := obj.GetAnnotations() diff --git a/internal/controllers/configuration/kongupstreampolicy_utils.go b/internal/controllers/configuration/kongupstreampolicy_utils.go index 6d54e34972..3fc1d6eca7 100644 --- a/internal/controllers/configuration/kongupstreampolicy_utils.go +++ b/internal/controllers/configuration/kongupstreampolicy_utils.go @@ -19,6 +19,8 @@ import ( incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1" ) +// maxNAncestors is the maximum number of ancestors that can be stored in the KongUpstreamPolicy status. +// This is a limitation of the Gateway API. const maxNAncestors = 16 // upstreamPolicyAncestorKind represents kind of KongUpstreamPolicy ancestor (Service or KongServiceFacade). @@ -32,10 +34,11 @@ const ( // ancestorStatus represents the status of an ancestor (Service or KongServiceFacade). // A collection of all ancestors' statuses is used to build the KongUpstreamPolicy status. type ancestorStatus struct { - namespacedName k8stypes.NamespacedName - ancestorKind upstreamPolicyAncestorKind - acceptedCondition metav1.Condition - creationTimestamp metav1.Time + namespacedName k8stypes.NamespacedName + ancestorKind upstreamPolicyAncestorKind + acceptedCondition metav1.Condition + programmedCondition metav1.Condition + creationTimestamp metav1.Time } // serviceKey is used as a key for indexing Services by "namespace/name". @@ -146,38 +149,61 @@ func (r *KongUpstreamPolicyReconciler) buildAncestorsStatus( Reason: string(gatewayapi.PolicyReasonAccepted), LastTransitionTime: metav1.Now(), } - conflictedCondition := metav1.Condition{ - Type: string(gatewayapi.PolicyConditionAccepted), - Status: metav1.ConditionFalse, - Reason: string(gatewayapi.PolicyReasonConflicted), + programmedCondition := metav1.Condition{ + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), LastTransitionTime: metav1.Now(), } // Build the status for each ancestor (Services and KongServiceFacades). ancestorsStatus := make([]ancestorStatus, 0, len(services)+len(serviceFacades)) for _, service := range services { - condition := acceptedCondition + service := service + acceptedCondition := acceptedCondition + programmedCondition := programmedCondition + if _, isConflicted := conflictedServices[buildServiceReference(service.Namespace, service.Name)]; isConflicted { - // If the service is conflicted, we will use the conflictedCondition. - condition = conflictedCondition + // If the Service is conflicted, we change both conditions to False. + acceptedCondition.Status = metav1.ConditionFalse + acceptedCondition.Reason = string(gatewayapi.PolicyReasonConflicted) + programmedCondition.Status = metav1.ConditionFalse + programmedCondition.Reason = string(gatewayapi.GatewayReasonPending) + } + + if !r.DataplaneClient.KubernetesObjectIsConfigured(&service) { + // If the Service is not configured, we change it to False. + programmedCondition.Status = metav1.ConditionFalse + programmedCondition.Reason = string(gatewayapi.GatewayReasonPending) } + ancestorsStatus = append(ancestorsStatus, ancestorStatus{ namespacedName: k8stypes.NamespacedName{ Namespace: service.Namespace, Name: service.Name, }, - ancestorKind: upstreamPolicyAncestorKindService, - acceptedCondition: condition, + ancestorKind: upstreamPolicyAncestorKindService, + acceptedCondition: acceptedCondition, + programmedCondition: programmedCondition, }) } for _, serviceFacade := range serviceFacades { + serviceFacade := serviceFacade + programmedCondition := programmedCondition + if !r.DataplaneClient.KubernetesObjectIsConfigured(&serviceFacade) { + // If the KongServiceFacade is not configured, we change it to False. + programmedCondition.Status = metav1.ConditionFalse + programmedCondition.Reason = string(gatewayapi.GatewayReasonPending) + } + ancestorsStatus = append(ancestorsStatus, ancestorStatus{ namespacedName: k8stypes.NamespacedName{ Namespace: serviceFacade.Namespace, Name: serviceFacade.Name, }, - ancestorKind: upstreamPolicyAncestorKindKongServiceFacade, - acceptedCondition: acceptedCondition, + ancestorKind: upstreamPolicyAncestorKindKongServiceFacade, + acceptedCondition: acceptedCondition, + programmedCondition: programmedCondition, }) } @@ -262,6 +288,9 @@ func getAllBackendRefsUsedWithService(httpRoute gatewayapi.HTTPRoute, serviceKey return backendRefs } +// buildPolicyStatus builds the KongUpstreamPolicy status from the ancestors' statuses. +// It ensures that the number of ancestors is not greater than the maximum allowed by the Gateway API +// and that the oldest ancestors are kept. func (r *KongUpstreamPolicyReconciler) buildPolicyStatus( upstreamPolicyNN k8stypes.NamespacedName, ancestorsStatus []ancestorStatus, @@ -271,7 +300,7 @@ func (r *KongUpstreamPolicyReconciler) buildPolicyStatus( return ancestorsStatus[i].creationTimestamp.Before(&ancestorsStatus[j].creationTimestamp) }) if len(ancestorsStatus) > maxNAncestors { - r.Log.Info("status has too many ancestors, the newest ones will be ignored", + r.Log.Info("status has more ancestors than the Gateway API permits, the newest ones will be ignored", "KongUpstreamPolicy", upstreamPolicyNN.String(), "ancestorsCount", len(ancestorsStatus), "maxAllowedAncestors", maxNAncestors, @@ -295,6 +324,7 @@ func (r *KongUpstreamPolicyReconciler) buildPolicyStatus( ControllerName: gatewaycontroller.GetControllerName(), Conditions: []metav1.Condition{ ss.acceptedCondition, + ss.programmedCondition, }, }, ) @@ -370,8 +400,12 @@ func buildServiceReference(namespace, name string) serviceKey { } func isSupportedHTTPRouteBackendRef(br gatewayapi.BackendRef) bool { - groupIsCoreOrNil := br.Group == nil || *br.Group == "core" + groupIsCoreOrNilOrEmpty := br.Group == nil || *br.Group == "core" || *br.Group == "" kindIsServiceOrNil := br.Kind == nil || *br.Kind == "Service" + // We only support core Services. - return groupIsCoreOrNil && kindIsServiceOrNil + // For Group the specification says when it's unspecified (nil or empty string), core API group should be inferred. + // For Kind nil case should never happen as it defaults on the API level to 'Service'. We can safely consider + // nil to be treated as 'Service' if it would happen for any reason. + return groupIsCoreOrNilOrEmpty && kindIsServiceOrNil } diff --git a/internal/controllers/configuration/kongupstreampolicy_utils_test.go b/internal/controllers/configuration/kongupstreampolicy_utils_test.go index 771108b39c..b23238fa13 100644 --- a/internal/controllers/configuration/kongupstreampolicy_utils_test.go +++ b/internal/controllers/configuration/kongupstreampolicy_utils_test.go @@ -18,6 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/kong/kubernetes-ingress-controller/v3/internal/controllers" gatewaycontroller "github.com/kong/kubernetes-ingress-controller/v3/internal/controllers/gateway" "github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi" "github.com/kong/kubernetes-ingress-controller/v3/internal/manager/scheme" @@ -37,6 +38,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { name string kongUpstreamPolicy kongv1beta1.KongUpstreamPolicy inputObjects []client.Object + objectsConfiguredInDataPlane bool expectedKongUpstreamPolicyStatus gatewayapi.PolicyStatus updated bool }{ @@ -100,6 +102,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { }, }, }, + objectsConfiguredInDataPlane: true, expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ Ancestors: []gatewayapi.PolicyAncestorStatus{ { @@ -116,6 +119,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, { @@ -132,6 +140,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, }, @@ -161,6 +174,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, { @@ -177,6 +195,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, }, @@ -234,6 +257,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { }, }, }, + objectsConfiguredInDataPlane: true, expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ Ancestors: []gatewayapi.PolicyAncestorStatus{ { @@ -250,6 +274,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, { @@ -266,6 +295,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, }, @@ -332,6 +366,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { }, }, }, + objectsConfiguredInDataPlane: true, expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ Ancestors: []gatewayapi.PolicyAncestorStatus{ { @@ -348,6 +383,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionFalse, Reason: string(gatewayapi.PolicyReasonConflicted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, }, @@ -418,6 +458,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { }, }, }, + objectsConfiguredInDataPlane: true, expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ Ancestors: []gatewayapi.PolicyAncestorStatus{ { @@ -434,6 +475,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, }, @@ -487,6 +533,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { }, }, }, + objectsConfiguredInDataPlane: true, expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ Ancestors: []gatewayapi.PolicyAncestorStatus{ { @@ -503,6 +550,11 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, }, }, { @@ -519,6 +571,107 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + }, + }, + }, + }, + }, + updated: true, + }, + { + name: "service and kong service facade not configured in data plane, programmed=false", + kongUpstreamPolicy: kongv1beta1.KongUpstreamPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: policyName, + Namespace: testNamespace, + }, + }, + inputObjects: []client.Object{ + &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: testNamespace, + Annotations: map[string]string{ + kongv1beta1.KongUpstreamPolicyAnnotationKey: policyName, + }, + CreationTimestamp: metav1.Now(), + }, + }, + &incubatorv1alpha1.KongServiceFacade{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-facade-1", + Namespace: testNamespace, + Annotations: map[string]string{ + kongv1beta1.KongUpstreamPolicyAnnotationKey: policyName, + }, + CreationTimestamp: metav1.Time{ + Time: metav1.Now().Add(10 * time.Second), + }, + }, + }, + &gatewayapi.HTTPRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "httpRoute", + Namespace: testNamespace, + }, + Spec: gatewayapi.HTTPRouteSpec{ + Rules: []gatewayapi.HTTPRouteRule{ + { + BackendRefs: []gatewayapi.HTTPBackendRef{ + builder.NewHTTPBackendRef("svc-1").Build(), + }, + }, + }, + }, + }, + }, + objectsConfiguredInDataPlane: false, + expectedKongUpstreamPolicyStatus: gatewayapi.PolicyStatus{ + Ancestors: []gatewayapi.PolicyAncestorStatus{ + { + AncestorRef: gatewayapi.ParentReference{ + Group: lo.ToPtr(gatewayapi.Group("core")), + Kind: lo.ToPtr(gatewayapi.Kind("Service")), + Namespace: lo.ToPtr(gatewayapi.Namespace(testNamespace)), + Name: gatewayapi.ObjectName("svc-1"), + }, + ControllerName: gatewaycontroller.GetControllerName(), + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.PolicyReasonAccepted), + }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi.GatewayReasonPending), + }, + }, + }, + { + AncestorRef: gatewayapi.ParentReference{ + Group: lo.ToPtr(gatewayapi.Group(incubatorv1alpha1.GroupVersion.Group)), + Kind: lo.ToPtr(gatewayapi.Kind(incubatorv1alpha1.KongServiceFacadeKind)), + Namespace: lo.ToPtr(gatewayapi.Namespace(testNamespace)), + Name: gatewayapi.ObjectName("svc-facade-1"), + }, + ControllerName: gatewaycontroller.GetControllerName(), + Conditions: []metav1.Condition{ + { + Type: string(gatewayapi.PolicyConditionAccepted), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.PolicyReasonAccepted), + }, + { + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionFalse, + Reason: string(gatewayapi.GatewayReasonPending), + }, }, }, }, @@ -543,6 +696,7 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { reconciler := KongUpstreamPolicyReconciler{ Client: fakeClient, + DataplaneClient: DataPlaneStatusClientMock{ObjectsConfigured: tc.objectsConfiguredInDataPlane}, KongServiceFacadeEnabled: true, } @@ -560,6 +714,15 @@ func TestEnforceKongUpstreamPolicyStatus(t *testing.T) { } } +type DataPlaneStatusClientMock struct { + controllers.DataPlane + ObjectsConfigured bool +} + +func (d DataPlaneStatusClientMock) KubernetesObjectIsConfigured(client.Object) bool { + return d.ObjectsConfigured +} + func TestHttpRouteHasUpstreamPolicyConflictedBackendRefsWithService(t *testing.T) { testCases := []struct { name string @@ -709,21 +872,28 @@ func TestBuildPolicyStatus(t *testing.T) { Status: metav1.ConditionTrue, Reason: string(gatewayapi.PolicyReasonAccepted), } + programmedCondition := metav1.Condition{ + Type: string(gatewayapi.GatewayConditionProgrammed), + Status: metav1.ConditionTrue, + Reason: string(gatewayapi.GatewayReasonProgrammed), + } serviceStatus := func(name string, creationTimestamp time.Time) ancestorStatus { return ancestorStatus{ - namespacedName: k8stypes.NamespacedName{Namespace: "default", Name: name}, - ancestorKind: upstreamPolicyAncestorKindService, - acceptedCondition: acceptedCondition, - creationTimestamp: metav1.NewTime(creationTimestamp), + namespacedName: k8stypes.NamespacedName{Namespace: "default", Name: name}, + ancestorKind: upstreamPolicyAncestorKindService, + acceptedCondition: acceptedCondition, + programmedCondition: programmedCondition, + creationTimestamp: metav1.NewTime(creationTimestamp), } } serviceFacadeStatus := func(name string, creationTimestamp time.Time) ancestorStatus { return ancestorStatus{ - namespacedName: k8stypes.NamespacedName{Namespace: "default", Name: name}, - ancestorKind: upstreamPolicyAncestorKindKongServiceFacade, - acceptedCondition: acceptedCondition, - creationTimestamp: metav1.NewTime(creationTimestamp), + namespacedName: k8stypes.NamespacedName{Namespace: "default", Name: name}, + ancestorKind: upstreamPolicyAncestorKindKongServiceFacade, + acceptedCondition: acceptedCondition, + programmedCondition: programmedCondition, + creationTimestamp: metav1.NewTime(creationTimestamp), } } @@ -738,6 +908,7 @@ func TestBuildPolicyStatus(t *testing.T) { ControllerName: gatewaycontroller.GetControllerName(), Conditions: []metav1.Condition{ acceptedCondition, + programmedCondition, }, } } @@ -752,6 +923,7 @@ func TestBuildPolicyStatus(t *testing.T) { ControllerName: gatewaycontroller.GetControllerName(), Conditions: []metav1.Condition{ acceptedCondition, + programmedCondition, }, } } @@ -873,6 +1045,21 @@ func TestIsSupportedHTTPRouteBackendRef(t *testing.T) { }, expected: false, }, + { + name: "empty group", + backendRef: gatewayapi.BackendObjectReference{ + Group: lo.ToPtr(gatewayapi.Group("")), + Kind: lo.ToPtr(gatewayapi.Kind("Service")), + }, + expected: true, + }, + { + name: "empty group with nil kind", + backendRef: gatewayapi.BackendObjectReference{ + Group: lo.ToPtr(gatewayapi.Group("")), + }, + expected: true, + }, } for _, tc := range testCases { diff --git a/internal/controllers/dataplane.go b/internal/controllers/dataplane.go index c484938775..63ff4cb5ba 100644 --- a/internal/controllers/dataplane.go +++ b/internal/controllers/dataplane.go @@ -13,8 +13,12 @@ import ( // with the Kong dataplane. type DataPlane interface { DataPlaneClient + DataPlaneStatusClient Listeners(ctx context.Context) ([]kong.ProxyListener, []kong.StreamListener, error) +} + +type DataPlaneStatusClient interface { AreKubernetesObjectReportsEnabled() bool KubernetesObjectConfigurationStatus(obj client.Object) k8sobj.ConfigurationStatus KubernetesObjectIsConfigured(obj client.Object) bool diff --git a/internal/dataplane/translator/ingressrules.go b/internal/dataplane/translator/ingressrules.go index c63b5798b2..16cb034aa1 100644 --- a/internal/dataplane/translator/ingressrules.go +++ b/internal/dataplane/translator/ingressrules.go @@ -358,6 +358,10 @@ func resolveKubernetesServiceForBackend( if err != nil { return nil, fmt.Errorf("failed to fetch Service %s/%s: %w", backend.Namespace(), backend.Name(), err) } + + // After Kubernetes Service is fetched successfully, we can consider it a translated object. + translatedObjectsCollector.Add(k8sService) + return k8sService, nil } diff --git a/internal/dataplane/translator/translator_test.go b/internal/dataplane/translator/translator_test.go index fd4bea28e3..8ed04969f1 100644 --- a/internal/dataplane/translator/translator_test.go +++ b/internal/dataplane/translator/translator_test.go @@ -4887,6 +4887,93 @@ func TestTranslator_ConfiguredKubernetesObjects(t *testing.T) { {Name: "consumer", Namespace: "bar"}, }, }, + { + name: "Ingress using Services and KongServiceFacades annotated with KongUpstreamPolicy", + objectsInStore: store.FakeObjects{ + KongUpstreamPolicies: []*kongv1beta1.KongUpstreamPolicy{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "upstream-policy1", + Namespace: "bar", + }, + }, + }, + Services: []*corev1.Service{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: "bar", + Annotations: map[string]string{ + kongv1beta1.KongUpstreamPolicyAnnotationKey: "upstream-policy1", + }, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Port: 80, + }, + }, + }, + }, + }, + KongServiceFacades: []*incubatorv1alpha1.KongServiceFacade{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "service-facade1", + Namespace: "bar", + Annotations: map[string]string{ + kongv1beta1.KongUpstreamPolicyAnnotationKey: "upstream-policy1", + }, + }, + Spec: incubatorv1alpha1.KongServiceFacadeSpec{ + Backend: incubatorv1alpha1.KongServiceFacadeBackend{ + Name: "service1", + Port: 80, + }, + }, + }, + }, + IngressesV1: []*netv1.Ingress{ + builder.NewIngress("ingress1", "kong"). + WithNamespace("bar"). + WithRules(netv1.IngressRule{ + IngressRuleValue: netv1.IngressRuleValue{ + HTTP: &netv1.HTTPIngressRuleValue{ + Paths: []netv1.HTTPIngressPath{ + { + Path: "/service", + Backend: netv1.IngressBackend{ + Service: &netv1.IngressServiceBackend{ + Name: "service1", + Port: netv1.ServiceBackendPort{ + Number: 80, + }, + }, + }, + }, + { + Path: "/service-facade", + Backend: netv1.IngressBackend{ + Resource: &corev1.TypedLocalObjectReference{ + Name: "service-facade1", + Kind: incubatorv1alpha1.KongServiceFacadeKind, + APIGroup: lo.ToPtr(incubatorv1alpha1.SchemeGroupVersion.Group), + }, + }, + }, + }, + }, + }, + }). + Build(), + }, + }, + expectedObjectsToBeConfigured: []k8stypes.NamespacedName{ + {Name: "ingress1", Namespace: "bar"}, + {Name: "service1", Namespace: "bar"}, + {Name: "service-facade1", Namespace: "bar"}, + }, + }, } for _, tc := range testCases { diff --git a/internal/manager/controllerdef.go b/internal/manager/controllerdef.go index 0eaaadcd6d..754a049c0c 100644 --- a/internal/manager/controllerdef.go +++ b/internal/manager/controllerdef.go @@ -270,6 +270,7 @@ func setupControllers( DataplaneClient: dataplaneClient, CacheSyncTimeout: c.CacheSyncTimeout, KongServiceFacadeEnabled: featureGates.Enabled(featuregates.KongServiceFacade) && c.KongServiceFacadeEnabled, + StatusQueue: kubernetesStatusQueue, }, }, },