From b513938ceca494f6eed2f083a45982ad5157eacc Mon Sep 17 00:00:00 2001 From: Tao Yi Date: Mon, 28 Oct 2024 20:17:22 +0800 Subject: [PATCH] Fix(konnect): reject reconciliation for incompatible cluster type of control plane (#810) * reject reconciliation for incompatible cluster type of control plane * add unit tests for handleControlPlaneRef * address comments --- .../konnect/reconciler_controlplaneref.go | 238 +++++++++++++++ .../reconciler_controlplaneref_test.go | 289 ++++++++++++++++++ controller/konnect/reconciler_generic.go | 193 ------------ 3 files changed, 527 insertions(+), 193 deletions(-) create mode 100644 controller/konnect/reconciler_controlplaneref.go create mode 100644 controller/konnect/reconciler_controlplaneref_test.go diff --git a/controller/konnect/reconciler_controlplaneref.go b/controller/konnect/reconciler_controlplaneref.go new file mode 100644 index 000000000..d9a7b8e8e --- /dev/null +++ b/controller/konnect/reconciler_controlplaneref.go @@ -0,0 +1,238 @@ +package konnect + +import ( + "context" + "fmt" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + "github.com/samber/lo" + "github.com/samber/mo" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kong/gateway-operator/controller/konnect/constraints" + "github.com/kong/gateway-operator/controller/pkg/patch" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func getControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( + e TEnt, +) mo.Option[configurationv1alpha1.ControlPlaneRef] { + none := mo.None[configurationv1alpha1.ControlPlaneRef]() + switch e := any(e).(type) { + case *configurationv1.KongConsumer: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1beta1.KongConsumerGroup: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongRoute: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongService: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongPluginBinding: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongUpstream: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongCACertificate: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongCertificate: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongVault: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongKey: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongKeySet: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongDataPlaneClientCertificate: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) + default: + return none + } +} + +// handleControlPlaneRef handles the ControlPlaneRef for the given entity. +// It sets the owner reference to the referenced ControlPlane and updates the +// status of the entity based on the referenced ControlPlane status. +func handleControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( + ctx context.Context, + cl client.Client, + ent TEnt, +) (ctrl.Result, error) { + cpRef, ok := getControlPlaneRef(ent).Get() + if !ok { + return ctrl.Result{}, nil + } + + switch cpRef.Type { + case configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef: + cp := konnectv1alpha1.KonnectGatewayControlPlane{} + // TODO(pmalek): handle cross namespace refs + nn := types.NamespacedName{ + Name: cpRef.KonnectNamespacedRef.Name, + Namespace: ent.GetNamespace(), + } + // Set namespace of control plane when it is non-empty. Only applyies for cluster scoped resources (KongVault). + if ent.GetNamespace() == "" && cpRef.KonnectNamespacedRef.Namespace != "" { + nn.Namespace = cpRef.KonnectNamespacedRef.Namespace + } + if err := cl.Get(ctx, nn, &cp); err != nil { + if res, errStatus := patch.StatusWithCondition( + ctx, cl, ent, + konnectv1alpha1.ControlPlaneRefValidConditionType, + metav1.ConditionFalse, + konnectv1alpha1.ControlPlaneRefReasonInvalid, + err.Error(), + ); errStatus != nil || !res.IsZero() { + return res, errStatus + } + if k8serrors.IsNotFound(err) { + return ctrl.Result{}, ReferencedControlPlaneDoesNotExistError{ + Reference: nn, + Err: err, + } + } + return ctrl.Result{}, err + } + + // Do not continue reconciling of the control plane has incompatible cluster type to prevent repeated failure of creation. + // Only CLUSTER_TYPE_CONTROL_PLANE and CLUSTER_TYPE_HYBRID are supported. + // The configuration in control plane group type are read only so they are unsupported to attach entities to them: + // https://docs.konghq.com/konnect/gateway-manager/control-plane-groups/#limitations + if cp.Spec.ClusterType != nil && + !lo.Contains(compatibleControlPlaneClusterTypes, *cp.Spec.ClusterType) { + if res, errStatus := patch.StatusWithCondition( + ctx, cl, ent, + konnectv1alpha1.ControlPlaneRefValidConditionType, + metav1.ConditionFalse, + konnectv1alpha1.ControlPlaneRefReasonInvalid, + fmt.Sprintf("Attaching to ControlPlane %s with cluster type %s is not supported", nn, *cp.Spec.ClusterType), + ); errStatus != nil || !res.IsZero() { + return res, errStatus + } + return ctrl.Result{}, nil + } + + cond, ok := k8sutils.GetCondition(konnectv1alpha1.KonnectEntityProgrammedConditionType, &cp) + if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() { + if res, errStatus := patch.StatusWithCondition( + ctx, cl, ent, + konnectv1alpha1.ControlPlaneRefValidConditionType, + metav1.ConditionFalse, + konnectv1alpha1.ControlPlaneRefReasonInvalid, + fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn), + ); errStatus != nil || !res.IsZero() { + return res, errStatus + } + + return ctrl.Result{Requeue: true}, nil + } + + var ( + old = ent.DeepCopyObject().(TEnt) + + // A cluster scoped object cannot set a namespaced object as its owner, and also we cannot set cross namespaced owner reference. + // So we skip setting owner reference for cluster scoped resources (KongVault). + // TODO: handle cross namespace refs + isNamespaceScoped = ent.GetNamespace() != "" + + // If an entity has another owner, we should not set the owner reference as that would prevent the entity from being deleted. + hasNoOwners = len(ent.GetOwnerReferences()) == 0 + ) + if isNamespaceScoped && hasNoOwners { + if err := controllerutil.SetOwnerReference(&cp, ent, cl.Scheme(), controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to set owner reference: %w", err) + } + } + + if err := cl.Patch(ctx, ent, client.MergeFrom(old)); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + if resource, ok := any(ent).(EntityWithControlPlaneRef); ok { + old := ent.DeepCopyObject().(TEnt) + resource.SetControlPlaneID(cp.Status.ID) + _, err := patch.ApplyStatusPatchIfNotEmpty(ctx, cl, ctrllog.FromContext(ctx), ent, old) + if err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, err + } + } + + if res, errStatus := patch.StatusWithCondition( + ctx, cl, ent, + konnectv1alpha1.ControlPlaneRefValidConditionType, + metav1.ConditionTrue, + konnectv1alpha1.ControlPlaneRefReasonValid, + fmt.Sprintf("Referenced ControlPlane %s is programmed", nn), + ); errStatus != nil || !res.IsZero() { + return res, errStatus + } + return ctrl.Result{}, nil + + default: + return ctrl.Result{}, fmt.Errorf("unimplemented ControlPlane ref type %q", cpRef.Type) + } +} + +func conditionMessageReferenceKonnectAPIAuthConfigurationInvalid(apiAuthRef types.NamespacedName) string { + return fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is invalid", apiAuthRef) +} + +func conditionMessageReferenceKonnectAPIAuthConfigurationValid(apiAuthRef types.NamespacedName) string { + return fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is valid", apiAuthRef) +} + +var compatibleControlPlaneClusterTypes = []sdkkonnectcomp.ClusterType{ + sdkkonnectcomp.ClusterTypeClusterTypeControlPlane, + sdkkonnectcomp.ClusterTypeClusterTypeHybrid, +} diff --git a/controller/konnect/reconciler_controlplaneref_test.go b/controller/konnect/reconciler_controlplaneref_test.go new file mode 100644 index 000000000..ff69c884a --- /dev/null +++ b/controller/konnect/reconciler_controlplaneref_test.go @@ -0,0 +1,289 @@ +package konnect + +import ( + "context" + "testing" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kong/gateway-operator/controller/konnect/constraints" + "github.com/kong/gateway-operator/modules/manager/scheme" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +type handleControlPlaneRefTestCase[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]] struct { + name string + ent TEnt + objects []client.Object + expectResult ctrl.Result + expectError bool + expectErrorContains string + // Returns true if the updated entity satisfy the assertion. + // Returns false and error message if entity fails to satisfy it. + updatedEntAssertions []func(TEnt) (ok bool, message string) +} + +func TestHandleControlPlaneRef(t *testing.T) { + + var ( + cpOK = &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "cp-ok", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12345", + }, + Conditions: []metav1.Condition{ + { + Type: konnectv1alpha1.KonnectEntityProgrammedConditionType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + cpGroup = &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "cp-group", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-group-12345", + }, + Conditions: []metav1.Condition{ + { + Type: konnectv1alpha1.KonnectEntityProgrammedConditionType, + Status: metav1.ConditionTrue, + }, + }, + }, + } + + cpNotProgrammed = &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "cp-not-programmed", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12345", + }, + Conditions: []metav1.Condition{ + { + Type: konnectv1alpha1.KonnectEntityProgrammedConditionType, + Status: metav1.ConditionFalse, + }, + }, + }, + } + + svcNoCPRef = &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "svc-no-cp-ref", + }, + } + + svcCPRefOK = &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "svc-cp-ok", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "cp-ok", + }, + }, + }, + } + + svcCPRefNotFound = &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "svc-cp-not-found", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "cp-not-found", + }, + }, + }, + } + + svcCPRefIncompatibleType = &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "svc-cp-incompatible", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "cp-group", + }, + }, + }, + } + + svcCPRefNotProgrammed = &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + Name: "svc-cp-not-programmed", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: "cp-not-programmed", + }, + }, + }, + } + ) + testCasesService := []handleControlPlaneRefTestCase[configurationv1alpha1.KongService, *configurationv1alpha1.KongService]{ + { + name: "no control plane ref", + ent: svcNoCPRef, + expectResult: ctrl.Result{}, + expectError: false, + }, + { + name: "control plane OK", + ent: svcCPRefOK, + objects: []client.Object{ + cpOK, + }, + expectResult: ctrl.Result{}, + expectError: false, + updatedEntAssertions: []func(svc *configurationv1alpha1.KongService) (ok bool, message string){ + func(svc *configurationv1alpha1.KongService) (bool, string) { + return svc.GetControlPlaneID() == "cp-12345" && lo.ContainsBy(svc.Status.Conditions, func(c metav1.Condition) bool { + return c.Type == konnectv1alpha1.ControlPlaneRefValidConditionType + }), "service should get control plane ID" + }, + func(svc *configurationv1alpha1.KongService) (bool, string) { + return lo.ContainsBy(svc.OwnerReferences, func(o metav1.OwnerReference) bool { + return o.Kind == "KonnectGatewayControlPlane" && o.Name == "cp-ok" + }), "service should have owner reference set to CP" + }, + func(svc *configurationv1alpha1.KongService) (bool, string) { + return lo.ContainsBy(svc.Status.Conditions, func(c metav1.Condition) bool { + return c.Type == konnectv1alpha1.ControlPlaneRefValidConditionType && c.Status == metav1.ConditionTrue + }), "service should have ControlPlaneRefValid set to true" + }, + }, + }, + { + name: "control plane not found", + ent: svcCPRefNotFound, + expectResult: ctrl.Result{}, + expectError: true, + expectErrorContains: `referenced Control Plane default/cp-not-found does not exist`, + updatedEntAssertions: []func(svc *configurationv1alpha1.KongService) (ok bool, message string){ + func(svc *configurationv1alpha1.KongService) (bool, string) { + return lo.ContainsBy(svc.Status.Conditions, func(c metav1.Condition) bool { + return c.Type == konnectv1alpha1.ControlPlaneRefValidConditionType && c.Status == metav1.ConditionFalse + }), "service should have ControlPlaneRefValid set to False" + }, + }, + }, + { + name: "control plane with incompatible cluster type (ControlPlane Group)", + ent: svcCPRefIncompatibleType, + objects: []client.Object{ + cpGroup, + }, + expectResult: ctrl.Result{}, + expectError: false, + updatedEntAssertions: []func(svc *configurationv1alpha1.KongService) (ok bool, message string){ + func(svc *configurationv1alpha1.KongService) (bool, string) { + return lo.ContainsBy(svc.Status.Conditions, func(c metav1.Condition) bool { + return c.Type == konnectv1alpha1.ControlPlaneRefValidConditionType && c.Status == metav1.ConditionFalse + }), "service should have ControlPlaneRefValid set to False" + }, + }, + }, + { + name: "control plane not programmed", + ent: svcCPRefNotProgrammed, + objects: []client.Object{ + cpNotProgrammed, + }, + expectResult: ctrl.Result{Requeue: true}, + expectError: false, + updatedEntAssertions: []func(svc *configurationv1alpha1.KongService) (ok bool, message string){ + func(svc *configurationv1alpha1.KongService) (bool, string) { + return lo.ContainsBy(svc.Status.Conditions, func(c metav1.Condition) bool { + return c.Type == konnectv1alpha1.ControlPlaneRefValidConditionType && c.Status == metav1.ConditionFalse + }), "service should have ControlPlaneRefValid set to False" + }, + }, + }, + } + + testHandleControlPlaenRef(t, testCasesService) +} + +func testHandleControlPlaenRef[ + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], +]( + t *testing.T, testCases []handleControlPlaneRefTestCase[T, TEnt], +) { + t.Helper() + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := scheme.Get() + require.NoError(t, configurationv1alpha1.AddToScheme(s)) + require.NoError(t, konnectv1alpha1.AddToScheme(s)) + + fakeClient := fake.NewClientBuilder(). + WithScheme(s). + WithObjects(tc.ent). + WithObjects(tc.objects...). + // WithStatusSubresource is required for updating status of handled entity. + WithStatusSubresource(tc.ent). + Build() + require.NoError(t, fakeClient.SubResource("status").Update(context.Background(), tc.ent)) + + res, err := handleControlPlaneRef(context.Background(), fakeClient, tc.ent) + + var updatedEnt TEnt = tc.ent.DeepCopyObject().(TEnt) + require.NoError(t, fakeClient.Get(context.Background(), client.ObjectKeyFromObject(tc.ent), updatedEnt)) + for _, assertion := range tc.updatedEntAssertions { + ok, msg := assertion(updatedEnt) + require.True(t, ok, msg) + } + + if tc.expectError { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectErrorContains) + return + } + + require.NoError(t, err) + require.Equal(t, tc.expectResult, res) + }) + } +} diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index 1b73d3188..228ba4542 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -27,7 +27,6 @@ import ( configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" - configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) @@ -930,195 +929,3 @@ func handleKongConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constr return ctrl.Result{}, nil } - -func getControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( - e TEnt, -) mo.Option[configurationv1alpha1.ControlPlaneRef] { - none := mo.None[configurationv1alpha1.ControlPlaneRef]() - switch e := any(e).(type) { - case *configurationv1.KongConsumer: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1beta1.KongConsumerGroup: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongRoute: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongService: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongPluginBinding: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongUpstream: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongCACertificate: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongCertificate: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongVault: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongKey: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongKeySet: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - case *configurationv1alpha1.KongDataPlaneClientCertificate: - if e.Spec.ControlPlaneRef == nil { - return none - } - return mo.Some(*e.Spec.ControlPlaneRef) - default: - return none - } -} - -// handleControlPlaneRef handles the ControlPlaneRef for the given entity. -// It sets the owner reference to the referenced ControlPlane and updates the -// status of the entity based on the referenced ControlPlane status. -func handleControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( - ctx context.Context, - cl client.Client, - ent TEnt, -) (ctrl.Result, error) { - cpRef, ok := getControlPlaneRef(ent).Get() - if !ok { - return ctrl.Result{}, nil - } - - switch cpRef.Type { - case configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef: - cp := konnectv1alpha1.KonnectGatewayControlPlane{} - // TODO(pmalek): handle cross namespace refs - nn := types.NamespacedName{ - Name: cpRef.KonnectNamespacedRef.Name, - Namespace: ent.GetNamespace(), - } - // Set namespace of control plane when it is non-empty. Only applyies for cluster scoped resources (KongVault). - if ent.GetNamespace() == "" && cpRef.KonnectNamespacedRef.Namespace != "" { - nn.Namespace = cpRef.KonnectNamespacedRef.Namespace - } - if err := cl.Get(ctx, nn, &cp); err != nil { - if res, errStatus := patch.StatusWithCondition( - ctx, cl, ent, - konnectv1alpha1.ControlPlaneRefValidConditionType, - metav1.ConditionFalse, - konnectv1alpha1.ControlPlaneRefReasonInvalid, - err.Error(), - ); errStatus != nil || !res.IsZero() { - return res, errStatus - } - if k8serrors.IsNotFound(err) { - return ctrl.Result{}, ReferencedControlPlaneDoesNotExistError{ - Reference: nn, - Err: err, - } - } - return ctrl.Result{}, err - } - - cond, ok := k8sutils.GetCondition(konnectv1alpha1.KonnectEntityProgrammedConditionType, &cp) - if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() { - if res, errStatus := patch.StatusWithCondition( - ctx, cl, ent, - konnectv1alpha1.ControlPlaneRefValidConditionType, - metav1.ConditionFalse, - konnectv1alpha1.ControlPlaneRefReasonInvalid, - fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn), - ); errStatus != nil || !res.IsZero() { - return res, errStatus - } - - return ctrl.Result{Requeue: true}, nil - } - - var ( - old = ent.DeepCopyObject().(TEnt) - - // A cluster scoped object cannot set a namespaced object as its owner, and also we cannot set cross namespaced owner reference. - // So we skip setting owner reference for cluster scoped resources (KongVault). - // TODO: handle cross namespace refs - isNamespaceScoped = ent.GetNamespace() != "" - - // If an entity has another owner, we should not set the owner reference as that would prevent the entity from being deleted. - hasNoOwners = len(ent.GetOwnerReferences()) == 0 - ) - if isNamespaceScoped && hasNoOwners { - if err := controllerutil.SetOwnerReference(&cp, ent, cl.Scheme(), controllerutil.WithBlockOwnerDeletion(true)); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to set owner reference: %w", err) - } - } - - if err := cl.Patch(ctx, ent, client.MergeFrom(old)); err != nil { - if k8serrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil - } - return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err) - } - - // TODO(pmalek): make this generic. - // CP ID is not stored in KonnectEntityStatus because not all entities - // have a ControlPlaneRef, hence the type constraints in the reconciler can't be used. - if resource, ok := any(ent).(EntityWithControlPlaneRef); ok { - old := ent.DeepCopyObject().(TEnt) - resource.SetControlPlaneID(cp.Status.ID) - _, err := patch.ApplyStatusPatchIfNotEmpty(ctx, cl, ctrllog.FromContext(ctx), ent, old) - if err != nil { - if k8serrors.IsConflict(err) { - return ctrl.Result{Requeue: true}, nil - } - return ctrl.Result{}, err - } - } - - if res, errStatus := patch.StatusWithCondition( - ctx, cl, ent, - konnectv1alpha1.ControlPlaneRefValidConditionType, - metav1.ConditionTrue, - konnectv1alpha1.ControlPlaneRefReasonValid, - fmt.Sprintf("Referenced ControlPlane %s is programmed", nn), - ); errStatus != nil || !res.IsZero() { - return res, errStatus - } - return ctrl.Result{}, nil - - default: - return ctrl.Result{}, fmt.Errorf("unimplemented ControlPlane ref type %q", cpRef.Type) - } -} - -func conditionMessageReferenceKonnectAPIAuthConfigurationInvalid(apiAuthRef types.NamespacedName) string { - return fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is invalid", apiAuthRef) -} - -func conditionMessageReferenceKonnectAPIAuthConfigurationValid(apiAuthRef types.NamespacedName) string { - return fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is valid", apiAuthRef) -}