From 5e144db7a117c0618abafbce79c41bd8a2ff3864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Thu, 1 Aug 2024 18:51:28 +0200 Subject: [PATCH] feat(konnect): add generic reconciler scaffolding (#446) * feat(konnect): add generic reconciler scaffolding * chore: add note about moving Konnect related status conditions * refactor: split APIAuthRefValid into 2 conditions: APIAuthValid and APIAuthResolvedRef * Update controller/konnect/constraints.go Co-authored-by: Mattia Lavacca * Update controller/konnect/constraints.go * Update controller/konnect/constraints.go * chore: go mod tidy --------- Co-authored-by: Mattia Lavacca --- .golangci.yaml | 1 + controller/konnect/conditions.go | 51 +++ controller/konnect/constraints.go | 43 +++ controller/konnect/entitytypename.go | 6 + controller/konnect/errors.go | 22 ++ controller/konnect/ops.go | 113 ++++++ controller/konnect/reconciler_generic.go | 331 ++++++++++++++++++ controller/konnect/reconciler_generic_test.go | 44 +++ controller/konnect/watch.go | 32 ++ go.mod | 9 +- go.sum | 8 +- hack/generators/go.mod | 4 +- modules/manager/scheme/scheme.go | 7 +- 13 files changed, 663 insertions(+), 8 deletions(-) create mode 100644 controller/konnect/conditions.go create mode 100644 controller/konnect/constraints.go create mode 100644 controller/konnect/entitytypename.go create mode 100644 controller/konnect/errors.go create mode 100644 controller/konnect/ops.go create mode 100644 controller/konnect/reconciler_generic.go create mode 100644 controller/konnect/reconciler_generic_test.go create mode 100644 controller/konnect/watch.go diff --git a/.golangci.yaml b/.golangci.yaml index 9d3f384dc..7b9ed978f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -38,6 +38,7 @@ linters-settings: sections: - standard - default + - prefix(github.com/kong/kubernetes-configuration) - prefix(github.com/kong/gateway-operator) importas: no-unaliased: true diff --git a/controller/konnect/conditions.go b/controller/konnect/conditions.go new file mode 100644 index 000000000..877442931 --- /dev/null +++ b/controller/konnect/conditions.go @@ -0,0 +1,51 @@ +package konnect + +// TODO(pmalek): move this to Konnect API directory so that it's part of the API contract. +// https://github.com/Kong/kubernetes-configuration/issues/14 + +const ( + // KonnectEntityProgrammedConditionType is the type of the condition that + // indicates whether the entity has been programmed in Konnect. + KonnectEntityProgrammedConditionType = "Programmed" + + // KonnectEntityProgrammedReason is the reason for the Programmed condition. + // It is set when the entity has been programmed in Konnect. + KonnectEntityProgrammedReason = "Programmed" +) + +const ( + // KonnectEntityAPIAuthConfigurationResolvedRefConditionType is the type of the + // condition that indicates whether the APIAuth configuration reference is + // valid and points to an existing APIAuth configuration. + KonnectEntityAPIAuthConfigurationResolvedRefConditionType = "APIAuthResolvedRef" + + // KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef is the reason + // used with the APIAuthResolvedRef condition type indicating that the APIAuth + // configuration reference has been resolved. + KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef = "ResolvedRef" + // KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound is the reason + // used with the APIAuthResolvedRef condition type indicating that the APIAuth + // configuration reference could not be resolved. + KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound = "RefNotFound" + // KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound is the reason + // used with the APIAuthResolvedRef condition type indicating that the APIAuth + // configuration reference is invalid and could not be resolved. + // Condition message can contain more information about the error. + KonnectEntityAPIAuthConfigurationResolvedRefReasonRefInvalid = "RefInvalid" +) + +const ( + // KonnectEntityAPIAuthConfigurationValidConditionType is the type of the + // condition that indicates whether the referenced APIAuth configuration is + // valid. + KonnectEntityAPIAuthConfigurationValidConditionType = "APIAuthValid" + + // KonnectEntityAPIAuthConfigurationReasonValid is the reason used with the + // APIAuthRefValid condition type indicating that the APIAuth configuration + // referenced by the entity is valid. + KonnectEntityAPIAuthConfigurationReasonValid = "Valid" + // KonnectEntityAPIAuthConfigurationReasonInvalid is the reason used with the + // APIAuthRefValid condition type indicating that the APIAuth configuration + // referenced by the entity is invalid. + KonnectEntityAPIAuthConfigurationReasonInvalid = "Invalid" +) diff --git a/controller/konnect/constraints.go b/controller/konnect/constraints.go new file mode 100644 index 000000000..4e4701c1b --- /dev/null +++ b/controller/konnect/constraints.go @@ -0,0 +1,43 @@ +package konnect + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + 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" +) + +// SupportedKonnectEntityType is an interface that all Konnect entity types +// must implement. +type SupportedKonnectEntityType interface { + configurationv1alpha1.KongService | + configurationv1alpha1.KongRoute | + configurationv1.KongConsumer | + configurationv1beta1.KongConsumerGroup + // TODO: add other types + + GetTypeName() string +} + +// EntityType is an interface that all Konnect entity types must implement. +// Separating this from SupportedKonnectEntityType allows us to use EntityType +// where client.Object is required, since it embeds client.Object and uses pointer +// to refer to the SupportedKonnectEntityType. +type EntityType[ + T SupportedKonnectEntityType, +] interface { + *T + + // Kubernetes Object methods + GetObjectMeta() metav1.Object + client.Object + + // Additional methods which are used in reconciling Konnect entities. + GetConditions() []metav1.Condition + SetConditions([]metav1.Condition) + GetKonnectStatus() *configurationv1alpha1.KonnectEntityStatus + GetKonnectAPIAuthConfigurationRef() konnectv1alpha1.KonnectAPIAuthConfigurationRef +} diff --git a/controller/konnect/entitytypename.go b/controller/konnect/entitytypename.go new file mode 100644 index 000000000..a75f73ff7 --- /dev/null +++ b/controller/konnect/entitytypename.go @@ -0,0 +1,6 @@ +package konnect + +func entityTypeName[T SupportedKonnectEntityType]() string { + var e T + return e.GetTypeName() +} diff --git a/controller/konnect/errors.go b/controller/konnect/errors.go new file mode 100644 index 000000000..70a812dfa --- /dev/null +++ b/controller/konnect/errors.go @@ -0,0 +1,22 @@ +package konnect + +import "fmt" + +// FailedKonnectOpError is an error type that is returned when an operation against +// Konnect API fails. +type FailedKonnectOpError[T SupportedKonnectEntityType] struct { + Op Op + Err error +} + +// Error implements the error interface. +func (e FailedKonnectOpError[T]) Error() string { + return fmt.Sprintf("failed to %s %s on Konnect: %v", + e.Op, entityTypeName[T](), e.Err, + ) +} + +// Unwrap returns the underlying error. +func (e FailedKonnectOpError[T]) Unwrap() error { + return e.Err +} diff --git a/controller/konnect/ops.go b/controller/konnect/ops.go new file mode 100644 index 000000000..84814a714 --- /dev/null +++ b/controller/konnect/ops.go @@ -0,0 +1,113 @@ +package konnect + +import ( + "context" + "fmt" + "time" + + sdkkonnectgo "github.com/Kong/sdk-konnect-go" + "github.com/go-logr/logr" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kong/gateway-operator/controller/pkg/log" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" +) + +// Op is the Konnect operation type. +type Op string + +const ( + // CreateOp is the Konnect create operation. + CreateOp Op = "create" + // UpdateOp is the Konnect update operation. + UpdateOp Op = "update" + // DeleteOp is the Konnect delete operation. + DeleteOp Op = "delete" +) + +// Create creates a Konnect entity. +func Create[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +](ctx context.Context, sdk *sdkkonnectgo.SDK, logger logr.Logger, cl client.Client, e *T) (*T, error) { + defer logOpComplete[T, TEnt](logger, time.Now(), CreateOp, e) + + switch ent := any(e).(type) { //nolint:gocritic + // --------------------------------------------------------------------- + // TODO: add other Konnect types + + default: + return nil, fmt.Errorf("unsupported entity type %T", ent) + } +} + +// Delete deletes a Konnect entity. +func Delete[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +](ctx context.Context, sdk *sdkkonnectgo.SDK, logger logr.Logger, cl client.Client, e *T) error { + defer logOpComplete[T, TEnt](logger, time.Now(), DeleteOp, e) + + switch ent := any(e).(type) { //nolint:gocritic + // --------------------------------------------------------------------- + // TODO: add other Konnect types + + default: + return fmt.Errorf("unsupported entity type %T", ent) + } +} + +// Update updates a Konnect entity. +func Update[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +](ctx context.Context, sdk *sdkkonnectgo.SDK, logger logr.Logger, cl client.Client, e *T) (ctrl.Result, error) { + var ( + ent = TEnt(e) + condProgrammed, ok = k8sutils.GetCondition(KonnectEntityProgrammedConditionType, ent) + now = time.Now() + timeFromLastUpdate = time.Since(condProgrammed.LastTransitionTime.Time) + ) + // If the entity is already programmed and the last update was less than + // the configured sync period, requeue after the remaining time. + if ok && + condProgrammed.Status == metav1.ConditionTrue && + condProgrammed.Reason == KonnectEntityProgrammedReason && + condProgrammed.ObservedGeneration == ent.GetObjectMeta().GetGeneration() && + timeFromLastUpdate <= configurableSyncPeriod { + requeueAfter := configurableSyncPeriod - timeFromLastUpdate + log.Debug(logger, "no need for update, requeueing after configured sync period", e, + "last_update", condProgrammed.LastTransitionTime.Time, + "time_from_last_update", timeFromLastUpdate, + "requeue_after", requeueAfter, + "requeue_at", now.Add(requeueAfter), + ) + return ctrl.Result{ + RequeueAfter: requeueAfter, + }, nil + } + + defer logOpComplete[T, TEnt](logger, now, UpdateOp, e) + + switch ent := any(e).(type) { //nolint:gocritic + // --------------------------------------------------------------------- + // TODO: add other Konnect types + + default: + return ctrl.Result{}, fmt.Errorf("unsupported entity type %T", ent) + } +} + +func logOpComplete[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +](logger logr.Logger, start time.Time, op Op, e TEnt) { + logger.Info("operation in Konnect API complete", + "op", op, + "duration", time.Since(start), + "type", entityTypeName[T](), + "konnect_id", e.GetKonnectStatus().GetKonnectID(), + ) +} diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go new file mode 100644 index 000000000..b3320a7e3 --- /dev/null +++ b/controller/konnect/reconciler_generic.go @@ -0,0 +1,331 @@ +package konnect + +import ( + "context" + "fmt" + "time" + + sdkkonnectgo "github.com/Kong/sdk-konnect-go" + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + 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" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kong/gateway-operator/controller/pkg/log" + "github.com/kong/gateway-operator/pkg/consts" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +const ( + // TODO(pmalek): make configurable https://github.com/Kong/gateway-operator/issues/451 + configurableSyncPeriod = 1 * time.Minute +) + +const ( + // KonnectCleanupFinalizer is the finalizer that is added to the Konnect + // entities when they are created in Konnect, and which is removed when + // the CR and Konnect entity are deleted. + KonnectCleanupFinalizer = "gateway.konghq.com/konnect-cleanup" +) + +// KonnectEntityReconciler reconciles a Konnect entities. +// It uses the generic type constraints to constrain the supported types. +type KonnectEntityReconciler[T SupportedKonnectEntityType, TEnt EntityType[T]] struct { + DevelopmentMode bool + Client client.Client +} + +// NewKonnectEntityReconciler returns a new KonnectEntityReconciler for the given +// Konnect entity type. +func NewKonnectEntityReconciler[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +]( + t T, + developmentMode bool, + client client.Client, +) *KonnectEntityReconciler[T, TEnt] { + return &KonnectEntityReconciler[T, TEnt]{ + DevelopmentMode: developmentMode, + Client: client, + } +} + +const ( + // MaxConcurrentReconciles is the maximum number of concurrent reconciles + // that the controller will allow. + MaxConcurrentReconciles = 8 +) + +// SetupWithManager sets up the controller with the given manager. +func (r *KonnectEntityReconciler[T, TEnt]) SetupWithManager(mgr ctrl.Manager) error { + var ( + e T + ent = TEnt(&e) + b = ctrl.NewControllerManagedBy(mgr). + For(ent). + Named(entityTypeName[T]()). + WithOptions(controller.Options{ + MaxConcurrentReconciles: MaxConcurrentReconciles, + }) + ) + + for _, dep := range ReconciliationWatchOptionsForEntity(r.Client, ent) { + b = dep(b) + } + return b.Complete(r) +} + +// Reconcile reconciles the given Konnect entity. +func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( + ctx context.Context, req ctrl.Request, +) (ctrl.Result, error) { + var ( + entityTypeName = entityTypeName[T]() + logger = log.GetLogger(ctx, entityTypeName, r.DevelopmentMode) + ) + + var ( + e T + ent = TEnt(&e) + ) + if err := r.Client.Get(ctx, req.NamespacedName, ent); err != nil { + if k8serrors.IsNotFound(err) { + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + log.Debug(logger, "reconciling", ent) + var ( + apiAuth konnectv1alpha1.KonnectAPIAuthConfiguration + apiAuthRef = types.NamespacedName{ + Name: ent.GetKonnectAPIAuthConfigurationRef().Name, + Namespace: ent.GetNamespace(), + // TODO(pmalek): enable if cross namespace refs are allowed + // Namespace: ent.GetKonnectAPIAuthConfigurationRef().Namespace, + } + ) + if err := r.Client.Get(ctx, apiAuthRef, &apiAuth); err != nil { + if k8serrors.IsNotFound(err) { + if res, err := updateStatusWithCondition( + ctx, r.Client, ent, + KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + metav1.ConditionFalse, + KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound, + fmt.Sprintf("Referenced KonnectAPIAuthConfiguration %s not found", apiAuthRef), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } + + if res, err := updateStatusWithCondition( + ctx, r.Client, ent, + KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + metav1.ConditionFalse, + KonnectEntityAPIAuthConfigurationResolvedRefReasonRefInvalid, + fmt.Sprintf("KonnectAPIAuthConfiguration reference %s is invalid: %v", apiAuthRef, err), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + + return ctrl.Result{}, fmt.Errorf("failed to get KonnectAPIAuthConfiguration: %w", err) + } + + // Update the status if the reference is resolved and it's not as expected. + if cond, present := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationResolvedRefConditionType, &apiAuth.Status); !present || + cond.Status != metav1.ConditionTrue || + cond.ObservedGeneration != apiAuth.GetGeneration() || + cond.Reason != KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef { + if res, err := updateStatusWithCondition( + ctx, r.Client, ent, + KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + metav1.ConditionTrue, + KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef, + fmt.Sprintf("KonnectAPIAuthConfiguration reference %s is resolved", apiAuthRef), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // Check if the referenced APIAuthConfiguration is valid. + if cond, present := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth.Status); !present || cond.Status != metav1.ConditionTrue { + if res, err := updateStatusWithCondition( + ctx, r.Client, ent, + KonnectEntityAPIAuthConfigurationValidConditionType, + metav1.ConditionFalse, + KonnectEntityAPIAuthConfigurationReasonInvalid, + fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is invalid", apiAuthRef), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + + return ctrl.Result{}, nil + } else if cond.ObservedGeneration != apiAuth.GetGeneration() || + cond.Reason != KonnectEntityAPIAuthConfigurationReasonValid { + // If the referenced APIAuthConfiguration is valid, set the "APIAuthValid" + // condition to true. Only perform the update if the Reason or ObservedGeneration + // has changed. + if res, err := updateStatusWithCondition( + ctx, r.Client, ent, + KonnectEntityAPIAuthConfigurationValidConditionType, + metav1.ConditionTrue, + KonnectEntityAPIAuthConfigurationReasonValid, + fmt.Sprintf("referenced KonnectAPIAuthConfiguration %s is valid", apiAuthRef), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + return ctrl.Result{}, nil + } + + // NOTE: We need to create a new SDK instance for each reconciliation + // because the token is retrieved in runtime through KonnectAPIAuthConfiguration. + sdk := sdkkonnectgo.New( + sdkkonnectgo.WithSecurity( + sdkkonnectgocomp.Security{ + PersonalAccessToken: sdkkonnectgo.String(apiAuth.Spec.Token), + }, + ), + sdkkonnectgo.WithServerURL("https://"+apiAuth.Spec.ServerURL), + ) + + if delTimestamp := ent.GetDeletionTimestamp(); !delTimestamp.IsZero() { + logger.Info("resource is being deleted") + // wait for termination grace period before cleaning up + if delTimestamp.After(time.Now()) { + logger.Info("resource still under grace period, requeueing") + return ctrl.Result{ + // Requeue when grace period expires. + // If deletion timestamp is changed, + // the update will trigger another round of reconciliation. + // so we do not consider updates of deletion timestamp here. + RequeueAfter: time.Until(delTimestamp.Time), + }, nil + } + + if controllerutil.RemoveFinalizer(ent, KonnectCleanupFinalizer) { + if err := Delete[T, TEnt](ctx, sdk, logger, r.Client, ent); err != nil { + return ctrl.Result{}, err + } + if err := r.Client.Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer %s: %w", KonnectCleanupFinalizer, err) + } + } + + return ctrl.Result{}, nil + } + + // TODO: relying on status ID is OK but we need to rethink this because + // we're using a cached client so after creating the resource on Konnect it might + // happen that we've just created the resource but the status ID is not there yet. + // + // We should look at the "expectations" for this: + // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/controller_utils.go + if id := ent.GetKonnectStatus().GetKonnectID(); id == "" { + _, err := Create[T, TEnt](ctx, sdk, logger, r.Client, ent) + if err != nil { + // TODO(pmalek): this is actually not 100% error prone because when status + // update fails we don't store the Konnect ID and hence the reconciler + // will try to create the resource again on next reconciliation. + if err := r.Client.Status().Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update status after creating object: %w", err) + } + + return ctrl.Result{}, FailedKonnectOpError[T]{ + Op: CreateOp, + Err: err, + } + } + + ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL + ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID + if err := r.Client.Status().Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + if controllerutil.AddFinalizer(ent, KonnectCleanupFinalizer) { + if err := r.Client.Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update finalizer: %w", err) + } + } + + // NOTE: we don't need to requeue here because the object update will trigger another reconciliation. + return ctrl.Result{}, nil + } + + if res, err := Update[T, TEnt](ctx, sdk, logger, r.Client, ent); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to update object: %w", err) + } else if res.Requeue || res.RequeueAfter > 0 { + return res, nil + } + + ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL + ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID + if err := r.Client.Status().Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update in cluster resource after Konnect update: %w", err) + } + + // NOTE: We requeue here to keep enforcing the state of the resource in Konnect. + // Konnect does not allow subscribing to changes so we need to keep pushing the + // desired state periodically. + return ctrl.Result{ + RequeueAfter: configurableSyncPeriod, + }, nil +} + +func updateStatusWithCondition[T SupportedKonnectEntityType, TEnt EntityType[T]]( + ctx context.Context, + client client.Client, + ent TEnt, + conditionType consts.ConditionType, + conditionStatus metav1.ConditionStatus, + conditionReason consts.ConditionReason, + conditionMessage string, +) (ctrl.Result, error) { + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditionType, + conditionStatus, + conditionReason, + conditionMessage, + ent.GetGeneration(), + ), + ent, + ) + + if err := client.Status().Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf( + "failed to update status with %s condition: %w", + KonnectEntityAPIAuthConfigurationResolvedRefConditionType, err, + ) + } + + return ctrl.Result{}, nil +} diff --git a/controller/konnect/reconciler_generic_test.go b/controller/konnect/reconciler_generic_test.go new file mode 100644 index 000000000..e60adfc33 --- /dev/null +++ b/controller/konnect/reconciler_generic_test.go @@ -0,0 +1,44 @@ +package konnect + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/kong/gateway-operator/modules/manager/scheme" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +func TestNewKonnectEntityReconciler(t *testing.T) { + testNewKonnectEntityReconciler(t, configurationv1.KongConsumer{}) + // GetTypeName() is missing. + // https://github.com/Kong/kubernetes-configuration/pull/15 fixes that. + // testNewKonnectEntityReconciler(t, configurationv1beta1.KongConsumerGroup{}) + testNewKonnectEntityReconciler(t, configurationv1alpha1.KongService{}) + testNewKonnectEntityReconciler(t, configurationv1alpha1.KongRoute{}) +} + +func testNewKonnectEntityReconciler[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +]( + t *testing.T, + ent T, +) { + t.Helper() + + t.Run(ent.GetTypeName(), func(t *testing.T) { + cl := fakectrlruntimeclient.NewFakeClient() + mgr, err := ctrl.NewManager(&rest.Config{}, ctrl.Options{ + Scheme: scheme.Get(), + }) + require.NoError(t, err) + reconciler := NewKonnectEntityReconciler[T, TEnt](ent, false, cl) + require.NoError(t, reconciler.SetupWithManager(mgr)) + }) +} diff --git a/controller/konnect/watch.go b/controller/konnect/watch.go new file mode 100644 index 000000000..2c0e3f10a --- /dev/null +++ b/controller/konnect/watch.go @@ -0,0 +1,32 @@ +package konnect + +import ( + "fmt" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +// ReconciliationWatchOptionsForEntity returns the watch options for the given +// Konnect entity type. +func ReconciliationWatchOptionsForEntity[ + T SupportedKonnectEntityType, + TEnt EntityType[T], +]( + cl client.Client, + ent TEnt, +) []func(*ctrl.Builder) *ctrl.Builder { + switch any(ent).(type) { + case *configurationv1alpha1.KongService: + return nil + case *configurationv1alpha1.KongRoute: + return nil + case *configurationv1.KongConsumer: + return nil + default: + panic(fmt.Sprintf("unsupported entity type %T", ent)) + } +} diff --git a/go.mod b/go.mod index b342e155f..e29f21074 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module github.com/kong/gateway-operator -go 1.22.3 +go 1.22.4 -toolchain go1.22.4 +toolchain go1.22.5 // 1.2.2 was released on main branch with a breaking change that was not // intended to be released in 1.2.x: @@ -17,6 +17,7 @@ require ( github.com/cloudflare/cfssl v1.6.5 github.com/go-logr/logr v1.4.2 github.com/google/uuid v1.6.0 + github.com/kong/kubernetes-configuration v0.0.0-20240801155137-c98bf64df469 github.com/kong/kubernetes-ingress-controller/v3 v3.2.3 github.com/kong/kubernetes-telemetry v0.1.4 github.com/kong/kubernetes-testing-framework v0.47.1 @@ -63,7 +64,7 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/homeport/dyff v1.6.0 // indirect github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 // indirect - github.com/kong/go-kong v0.56.0 // indirect + github.com/kong/go-kong v0.57.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-ciede2000 v0.0.0-20170301095244-782e8c62fec3 // indirect @@ -186,7 +187,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.30.1 + k8s.io/apiextensions-apiserver v0.30.3 k8s.io/component-base v0.30.3 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect diff --git a/go.sum b/go.sum index 5777ea2f6..63c0da748 100644 --- a/go.sum +++ b/go.sum @@ -215,8 +215,12 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kong/go-kong v0.56.0 h1:/9qbnQJWAgrSAKzL2RViBhHMTYOEyG8N4ClkKnUwEW4= -github.com/kong/go-kong v0.56.0/go.mod h1:gyNwyP1fzztT6sX/0/ygMQ30OiRMIQ51b2jSfstMrcU= +github.com/kong/go-kong v0.57.0 h1:e+4bHTzcO0xhFPVyGIUtlO+B4E4l14k55NH8Vjw6ORY= +github.com/kong/go-kong v0.57.0/go.mod h1:gyNwyP1fzztT6sX/0/ygMQ30OiRMIQ51b2jSfstMrcU= +github.com/kong/kubernetes-configuration v0.0.0-20240801095618-0ed30e4054cb h1:GUMgT3qqNjLV1Wdl8D6fskEaN/vof/FRhRbicIkUKpg= +github.com/kong/kubernetes-configuration v0.0.0-20240801095618-0ed30e4054cb/go.mod h1:kZTKzwQ68Wk2n8W8Em0RsYTL2yVNbCWU+5b9w1WU+Hs= +github.com/kong/kubernetes-configuration v0.0.0-20240801155137-c98bf64df469 h1:wzRoAf4byqST4BvmqPne/GL56VrnsrRM3A8ePuvOyf4= +github.com/kong/kubernetes-configuration v0.0.0-20240801155137-c98bf64df469/go.mod h1:kZTKzwQ68Wk2n8W8Em0RsYTL2yVNbCWU+5b9w1WU+Hs= github.com/kong/kubernetes-ingress-controller/v3 v3.2.3 h1:SQ/0hfceGmsvzbkCUxiJUv1ELcFRp4d6IzvYGfHct9o= github.com/kong/kubernetes-ingress-controller/v3 v3.2.3/go.mod h1:gshVZnDU2FTe/95I3vSJPsH2kyB8zR+GpUIieCyt8C4= github.com/kong/kubernetes-telemetry v0.1.4 h1:Yz7OlECxWKgNRG1wJ5imA4+H0dQEpdU9d86uhwUVpu4= diff --git a/hack/generators/go.mod b/hack/generators/go.mod index 4ccaf6a37..2ffb6ca16 100644 --- a/hack/generators/go.mod +++ b/hack/generators/go.mod @@ -1,6 +1,8 @@ module github.com/kong/gateway-operator/hack/generators -go 1.22.3 +go 1.22.4 + +toolchain go1.22.5 replace github.com/kong/gateway-operator => ../../ diff --git a/modules/manager/scheme/scheme.go b/modules/manager/scheme/scheme.go index dc6d0a2ce..8d234eaa0 100644 --- a/modules/manager/scheme/scheme.go +++ b/modules/manager/scheme/scheme.go @@ -1,7 +1,6 @@ package scheme import ( - configurationv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -10,6 +9,10 @@ import ( operatorv1alpha1 "github.com/kong/gateway-operator/api/v1alpha1" operatorv1beta1 "github.com/kong/gateway-operator/api/v1beta1" + + 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" ) // Get returns a scheme aware of all types the manager can interact with. @@ -21,5 +24,7 @@ func Get() *runtime.Scheme { utilruntime.Must(gatewayv1.Install(scheme)) utilruntime.Must(gatewayv1beta1.Install(scheme)) utilruntime.Must(configurationv1.AddToScheme(scheme)) + utilruntime.Must(configurationv1beta1.AddToScheme(scheme)) + utilruntime.Must(configurationv1alpha1.AddToScheme(scheme)) return scheme }