Skip to content

Commit

Permalink
feat(konnect): add support for serviceless KongRoutes (#669)
Browse files Browse the repository at this point in the history
* feat(konnect): allow serviceless KongRoutes

* tests: add serviceref tests

* Update controller/konnect/ops/ops_kongroute.go

Co-authored-by: Grzegorz Burzyński <[email protected]>

* chore: fix build

---------

Co-authored-by: Grzegorz Burzyński <[email protected]>
  • Loading branch information
pmalek and czeslavo authored Sep 30, 2024
1 parent f711d75 commit 9c6420b
Show file tree
Hide file tree
Showing 13 changed files with 596 additions and 217 deletions.
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,6 @@
the creation of a managed `KongPluginBinding` resource:
- `KongService` [#550](https://github.com/Kong/gateway-operator/pull/550)
- `KongRoute` [#644](https://github.com/Kong/gateway-operator/pull/644)
- `KongConsumer` [#652](https://github.com/Kong/gateway-operator/pull/652)
- `KongConsumerGroup` [#654](https://github.com/Kong/gateway-operator/pull/654)
These `KongPluginBinding`s are taken by the `KongPluginBinding` reconciler
to create the corresponding plugin objects in Konnect.
- `KongConsumer` associated with `ConsumerGroups` is now reconciled in Konnect by removing/adding
Expand All @@ -93,6 +91,8 @@
- basic-auth [#625](https://github.com/Kong/gateway-operator/pull/625)
- API key [#635](https://github.com/Kong/gateway-operator/pull/635)
- ACL [#661](https://github.com/Kong/gateway-operator/pull/661)
- Add support for `KongRoute`s bound directly to `KonnectGatewayControlPlane`s (serviceless rotues).
[#669](https://github.com/Kong/gateway-operator/pull/669)

### Fixed

Expand Down
39 changes: 39 additions & 0 deletions config/samples/konnect_kongroute_controlplane_ref.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
kind: KonnectAPIAuthConfiguration
apiVersion: konnect.konghq.com/v1alpha1
metadata:
name: konnect-api-auth-dev-1
namespace: default
spec:
type: token
token: kpat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
serverURL: us.api.konghq.com
---
kind: KonnectGatewayControlPlane
apiVersion: konnect.konghq.com/v1alpha1
metadata:
name: test1
namespace: default
spec:
name: test1
labels:
app: test1
key1: test1
konnect:
authRef:
name: konnect-api-auth-dev-1
---
kind: KongRoute
apiVersion: configuration.konghq.com/v1alpha1
metadata:
name: service-1
namespace: default
spec:
name: route-1
protocols:
- http
hosts:
- example.com
controlPlaneRef:
type: konnectNamespacedRef
konnectNamespacedRef:
name: test1
9 changes: 6 additions & 3 deletions controller/konnect/ops/ops_kongroute.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func kongRouteToSDKRouteInput(
// Deduplicate tags to avoid rejection by Konnect.
tags := lo.Uniq(slices.Concat(specTags, annotationTags, k8sTags))

return sdkkonnectcomp.RouteInput{
r := sdkkonnectcomp.RouteInput{
Destinations: route.Spec.KongRouteAPISpec.Destinations,
Headers: route.Spec.KongRouteAPISpec.Headers,
Hosts: route.Spec.KongRouteAPISpec.Hosts,
Expand All @@ -140,8 +140,11 @@ func kongRouteToSDKRouteInput(
Sources: route.Spec.KongRouteAPISpec.Sources,
StripPath: route.Spec.KongRouteAPISpec.StripPath,
Tags: tags,
Service: &sdkkonnectcomp.RouteService{
}
if route.Status.Konnect != nil && route.Status.Konnect.ServiceID != "" {
r.Service = &sdkkonnectcomp.RouteService{
ID: sdkkonnectgo.String(route.Status.Konnect.ServiceID),
},
}
}
return r
}
197 changes: 17 additions & 180 deletions controller/konnect/reconciler_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -667,175 +667,6 @@ func getConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constraints.E
}
}

func getServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]](
e TEnt,
) mo.Option[configurationv1alpha1.ServiceRef] {
switch e := any(e).(type) {
case *configurationv1alpha1.KongRoute:
if e.Spec.ServiceRef == nil {
return mo.None[configurationv1alpha1.ServiceRef]()
}
return mo.Some(*e.Spec.ServiceRef)
default:
return mo.None[configurationv1alpha1.ServiceRef]()
}
}

// handleKongServiceRef handles the ServiceRef for the given entity.
// It sets the owner reference to the referenced KongService and updates the
// status of the entity based on the referenced KongService status.
func handleKongServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]](
ctx context.Context,
cl client.Client,
ent TEnt,
) (ctrl.Result, error) {
kongServiceRef, ok := getServiceRef(ent).Get()
if !ok {
return ctrl.Result{}, nil
}
switch kongServiceRef.Type {
case configurationv1alpha1.ServiceRefNamespacedRef:
svc := configurationv1alpha1.KongService{}
nn := types.NamespacedName{
Name: kongServiceRef.NamespacedRef.Name,
// TODO: handle cross namespace refs
Namespace: ent.GetNamespace(),
}

if err := cl.Get(ctx, nn, &svc); err != nil {
if res, errStatus := updateStatusWithCondition(
ctx, cl, ent,
conditions.KongServiceRefValidConditionType,
metav1.ConditionFalse,
conditions.KongServiceRefReasonInvalid,
err.Error(),
); errStatus != nil || !res.IsZero() {
return res, errStatus
}

return ctrl.Result{}, fmt.Errorf("can't get the referenced KongService %s: %w", nn, err)
}

// If referenced KongService is being deleted, return an error so that we
// can remove the entity from Konnect first.
if delTimestamp := svc.GetDeletionTimestamp(); !delTimestamp.IsZero() {
return ctrl.Result{}, ReferencedKongServiceIsBeingDeleted{
Reference: nn,
}
}

cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, &svc)
if !ok || cond.Status != metav1.ConditionTrue {
ent.SetKonnectID("")
if res, err := updateStatusWithCondition(
ctx, cl, ent,
conditions.KongServiceRefValidConditionType,
metav1.ConditionFalse,
conditions.KongServiceRefReasonInvalid,
fmt.Sprintf("Referenced KongService %s is not programmed yet", nn),
); err != nil || !res.IsZero() {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}

old := ent.DeepCopyObject().(TEnt)
if err := controllerutil.SetOwnerReference(&svc, 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.
// Service ID is not stored in KonnectEntityStatus because not all entities
// have a ServiceRef, hence the type constraints in the reconciler can't be used.
if route, ok := any(ent).(*configurationv1alpha1.KongRoute); ok {
if route.Status.Konnect == nil {
route.Status.Konnect = &konnectv1alpha1.KonnectEntityStatusWithControlPlaneAndServiceRefs{}
}
route.Status.Konnect.ServiceID = svc.Status.Konnect.GetKonnectID()
}

if res, errStatus := updateStatusWithCondition(
ctx, cl, ent,
conditions.KongServiceRefValidConditionType,
metav1.ConditionTrue,
conditions.KongServiceRefReasonValid,
fmt.Sprintf("Referenced KongService %s programmed", nn),
); errStatus != nil || !res.IsZero() {
return res, errStatus
}

cpRef, ok := getControlPlaneRef(&svc).Get()
if !ok {
return ctrl.Result{}, fmt.Errorf(
"KongRoute references a KongService %s which does not have a ControlPlane ref",
client.ObjectKeyFromObject(&svc),
)
}
cp, err := getCPForRef(ctx, cl, cpRef, ent.GetNamespace())
if err != nil {
if res, errStatus := updateStatusWithCondition(
ctx, cl, ent,
conditions.ControlPlaneRefValidConditionType,
metav1.ConditionFalse,
conditions.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(conditions.KonnectEntityProgrammedConditionType, cp)
if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() {
if res, errStatus := updateStatusWithCondition(
ctx, cl, ent,
conditions.ControlPlaneRefValidConditionType,
metav1.ConditionFalse,
conditions.ControlPlaneRefReasonInvalid,
fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn),
); errStatus != nil || !res.IsZero() {
return res, errStatus
}

return ctrl.Result{Requeue: true}, nil
}

// 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 {
resource.SetControlPlaneID(cp.Status.ID)
}

if res, errStatus := updateStatusWithCondition(
ctx, cl, ent,
conditions.ControlPlaneRefValidConditionType,
metav1.ConditionTrue,
conditions.ControlPlaneRefReasonValid,
fmt.Sprintf("Referenced ControlPlane %s is programmed", nn),
); errStatus != nil || !res.IsZero() {
return res, errStatus
}

default:
return ctrl.Result{}, fmt.Errorf("unimplemented KongService ref type %q", kongServiceRef.Type)
}

return ctrl.Result{}, nil
}

// handleKongConsumerRef handles the ConsumerRef for the given entity.
// It sets the owner reference to the referenced KongConsumer and updates the
// status of the entity based on the referenced KongConsumer status.
Expand Down Expand Up @@ -1000,59 +831,65 @@ func handleKongConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constr
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 mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1beta1.KongConsumerGroup:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
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 mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongPluginBinding:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongUpstream:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongCACertificate:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongCertificate:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongVault:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongKey:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
case *configurationv1alpha1.KongKeySet:
if e.Spec.ControlPlaneRef == nil {
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
return mo.Some(*e.Spec.ControlPlaneRef)
default:
return mo.None[configurationv1alpha1.ControlPlaneRef]()
return none
}
}

Expand Down
Loading

0 comments on commit 9c6420b

Please sign in to comment.