Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(konnect): fix reconciling entities when they got created on Konnect but an error was returned #716

Merged
merged 2 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions controller/konnect/index_konnectgatewaycontrolplane.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package konnect

import (
sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components"
"sigs.k8s.io/controller-runtime/pkg/client"

konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1"
)

const (
// IndexFieldKonnectGatewayControlPlaneGroupOnMembers is the index field for KonnectGatewayControlPlane -> its members.
IndexFieldKonnectGatewayControlPlaneGroupOnMembers = "konnectGatewayControlPlaneGroupMembers"

// IndexFieldKonnectGatewayControlPlaneOnAPIAuthConfiguration is the index field for KonnectGatewayControlPlane -> APIAuthConfiguration.
IndexFieldKonnectGatewayControlPlaneOnAPIAuthConfiguration = "konnectGatewayControlPlaneAPIAuthConfigurationRef"
)

// IndexOptionsForKonnectGatewayControlPlane returns required Index options for KonnectGatewayControlPlane reconciler.
func IndexOptionsForKonnectGatewayControlPlane() []ReconciliationIndexOption {
return []ReconciliationIndexOption{
{
IndexObject: &konnectv1alpha1.KonnectGatewayControlPlane{},
IndexField: IndexFieldKonnectGatewayControlPlaneGroupOnMembers,
ExtractValue: konnectGatewayControlPlaneGroupMembers,
},
{
IndexObject: &konnectv1alpha1.KonnectGatewayControlPlane{},
IndexField: IndexFieldKonnectGatewayControlPlaneOnAPIAuthConfiguration,
Expand All @@ -22,6 +31,28 @@ func IndexOptionsForKonnectGatewayControlPlane() []ReconciliationIndexOption {
}
}

func konnectGatewayControlPlaneGroupMembers(object client.Object) []string {
cp, ok := object.(*konnectv1alpha1.KonnectGatewayControlPlane)
if !ok {
return nil
}
clusterType := cp.Spec.ClusterType
if clusterType == nil {
return nil
}

if string(*clusterType) != string(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup) {
return nil
}

ret := make([]string, 0, len(cp.Spec.Members))
for _, member := range cp.Spec.Members {
ret = append(ret, member.Name)
}

return ret
}

func konnectGatewayControlPlaneAPIAuthConfigurationRef(object client.Object) []string {
cp, ok := object.(*konnectv1alpha1.KonnectGatewayControlPlane)
if !ok {
Expand Down
8 changes: 4 additions & 4 deletions controller/konnect/ops/ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,10 @@ func shouldUpdate[
requeueAfter := syncPeriod - timeFromLastUpdate
log.Debug(ctrllog.FromContext(ctx),
"no need for update, requeueing after configured sync period", ent,
"last_update", condProgrammed.LastTransitionTime.Time,
"time_from_last_update", timeFromLastUpdate,
"requeue_after", requeueAfter,
"requeue_at", now.Add(requeueAfter),
"last_update", condProgrammed.LastTransitionTime.Time.String(),
"time_from_last_update", timeFromLastUpdate.String(),
"requeue_after", requeueAfter.String(),
"requeue_at", now.Add(requeueAfter).String(),
)
return false, ctrl.Result{
RequeueAfter: requeueAfter,
Expand Down
75 changes: 49 additions & 26 deletions controller/konnect/reconciler_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -483,39 +483,54 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile(
// We should look at the "expectations" for this:
// https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/controller_utils.go
if status := ent.GetKonnectStatus(); status == nil || status.GetKonnectID() == "" {
obj := ent.DeepCopyObject().(client.Object)
_, err := ops.Create[T, TEnt](ctx, sdk, 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

// TODO: 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.

// Regardless of the error reported from Create(), if the Konnect ID has been
// set then add the finalizer so that the resource can be cleaned up from Konnect on deletion.
if ent.GetKonnectStatus().ID != "" {
objWithFinalizer := ent.DeepCopyObject().(client.Object)
if controllerutil.AddFinalizer(objWithFinalizer, KonnectCleanupFinalizer) {
if errUpd := r.Client.Patch(ctx, objWithFinalizer, client.MergeFrom(ent)); errUpd != nil {
if k8serrors.IsConflict(errUpd) {
return ctrl.Result{Requeue: true}, nil
}
if err != nil {
return ctrl.Result{}, fmt.Errorf(
"failed to update finalizer %s: %w, object create operation failed against Konnect API: %w",
KonnectCleanupFinalizer, errUpd, err,
)
}
return ctrl.Result{}, fmt.Errorf(
"failed to update finalizer %s: %w",
KonnectCleanupFinalizer, errUpd,
)
}
return ctrl.Result{}, fmt.Errorf("failed to update status after creating object: %w", err)
}
}

return ctrl.Result{}, ops.FailedKonnectOpError[T]{
Op: ops.CreateOp,
Err: err,
}
if err == nil {
setServerURLAndOrgIDFromAPIAuthConfiguration(ent, apiAuth)
}

ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL
ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID
if err := r.Client.Status().Update(ctx, ent); err != nil {
// Regardless of the error, set the status as it can contain the Konnect ID
// and/or status conditions set in Create() and that ID will be needed to
// update/delete the object in Konnect.
if err := r.Client.Status().Patch(ctx, ent, client.MergeFrom(obj)); err != nil {
if k8serrors.IsConflict(err) {
return ctrl.Result{Requeue: true}, nil
}
return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err)
return ctrl.Result{}, fmt.Errorf("failed to update status after creating object: %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)
if err != nil {
return ctrl.Result{}, ops.FailedKonnectOpError[T]{
Op: ops.CreateOp,
Err: err,
}
}

Expand All @@ -524,8 +539,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile(
}

if res, err := ops.Update[T, TEnt](ctx, sdk, r.SyncPeriod, r.Client, ent); err != nil {
ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL
ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID
setServerURLAndOrgIDFromAPIAuthConfiguration(ent, apiAuth)
if errUpd := r.Client.Status().Update(ctx, ent); errUpd != nil {
if k8serrors.IsConflict(errUpd) {
return ctrl.Result{Requeue: true}, nil
Expand All @@ -538,8 +552,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile(
return res, nil
}

ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL
ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID
setServerURLAndOrgIDFromAPIAuthConfiguration(ent, apiAuth)
if err := r.Client.Status().Update(ctx, ent); err != nil {
if k8serrors.IsConflict(err) {
return ctrl.Result{Requeue: true}, nil
Expand All @@ -555,6 +568,16 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile(
}, nil
}

func setServerURLAndOrgIDFromAPIAuthConfiguration(
ent interface {
GetKonnectStatus() *konnectv1alpha1.KonnectEntityStatus
},
apiAuth konnectv1alpha1.KonnectAPIAuthConfiguration,
) {
ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL
ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID
}

// EntityWithControlPlaneRef is an interface for entities that have a ControlPlaneRef.
type EntityWithControlPlaneRef interface {
SetControlPlaneID(string)
Expand Down
32 changes: 32 additions & 0 deletions controller/konnect/watch_konnectcontrolplane.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,38 @@ func KonnectGatewayControlPlaneReconciliationWatchOptions(
),
)
},
func(b *ctrl.Builder) *ctrl.Builder {
return b.Watches(
&konnectv1alpha1.KonnectGatewayControlPlane{},
handler.EnqueueRequestsFromMapFunc(
enqueueKonnectGatewayControlPlaneGroupForMembers(cl),
),
)
},
}
}

func enqueueKonnectGatewayControlPlaneGroupForMembers(
cl client.Client,
) func(ctx context.Context, obj client.Object) []reconcile.Request {
return func(ctx context.Context, obj client.Object) []reconcile.Request {
cp, ok := obj.(*konnectv1alpha1.KonnectGatewayControlPlane)
if !ok {
return nil
}
var l konnectv1alpha1.KonnectGatewayControlPlaneList
if err := cl.List(ctx, &l,
// TODO: change this when cross namespace refs are allowed.
client.InNamespace(cp.GetNamespace()),
client.MatchingFields{
// List groups that this control plane is a member of.
IndexFieldKonnectGatewayControlPlaneGroupOnMembers: cp.Name,
},
); err != nil {
return nil
}

return objectListToReconcileRequests(l.Items)
}
}

Expand Down
Loading
Loading