Skip to content

Commit

Permalink
feat(konnect): add generic reconciler scaffolding (#446)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Update controller/konnect/constraints.go

* Update controller/konnect/constraints.go

* chore: go mod tidy

---------

Co-authored-by: Mattia Lavacca <[email protected]>
  • Loading branch information
pmalek and mlavacca authored Aug 1, 2024
1 parent e040c10 commit 5e144db
Show file tree
Hide file tree
Showing 13 changed files with 663 additions and 8 deletions.
1 change: 1 addition & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions controller/konnect/conditions.go
Original file line number Diff line number Diff line change
@@ -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"
)
43 changes: 43 additions & 0 deletions controller/konnect/constraints.go
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 6 additions & 0 deletions controller/konnect/entitytypename.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package konnect

func entityTypeName[T SupportedKonnectEntityType]() string {
var e T
return e.GetTypeName()
}
22 changes: 22 additions & 0 deletions controller/konnect/errors.go
Original file line number Diff line number Diff line change
@@ -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
}
113 changes: 113 additions & 0 deletions controller/konnect/ops.go
Original file line number Diff line number Diff line change
@@ -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(),
)
}
Loading

0 comments on commit 5e144db

Please sign in to comment.