Skip to content

Commit

Permalink
feat: introduce basic reconciliation for KongPluginInstallation CRD (#…
Browse files Browse the repository at this point in the history
…424)


Co-authored-by: Mattia Lavacca <[email protected]>
Co-authored-by: Grzegorz Burzyński <[email protected]>
  • Loading branch information
3 people authored Aug 9, 2024
1 parent ec80e42 commit cdfb65a
Show file tree
Hide file tree
Showing 24 changed files with 809 additions and 31 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ jobs:
- name: run unit tests
run: make test.unit
env:
KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS: ${{ secrets.KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS }}
GOTESTSUM_JUNITFILE: "unit-tests.xml"

- name: collect test coverage
Expand Down Expand Up @@ -250,6 +251,7 @@ jobs:
run: make test.integration
env:
KONG_TEST_DISABLE_CERTMANAGER: "true"
KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS: ${{ secrets.KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS }}
WEBHOOK_ENABLED: ${{ matrix.webhook-enabled }}
KONG_CONTROLLER_OUT: stdout
GOTESTSUM_JUNITFILE: integration-tests-webhook-enabled-${{ matrix.webhook-enabled }}.xml
Expand Down Expand Up @@ -353,6 +355,7 @@ jobs:
env:
KONG_TEST_DISABLE_CERTMANAGER: "true"
KONG_CONTROLLER_OUT: stdout
KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS: ${{ secrets.KONG_PLUGIN_IMAGE_REGISTRY_CREDENTIALS }}
GOTESTSUM_JUNITFILE: integration-tests-provision-dataplane-fail.xml
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
[#387](https://github.com/Kong/gateway-operator/pull/387)
- Introduce `KongPluginInstallation` CRD to allow installing custom Kong
plugins distributed as container images.
[400](https://github.com/Kong/gateway-operator/pull/400)
[400](https://github.com/Kong/gateway-operator/pull/400), [424](https://github.com/Kong/gateway-operator/pull/424)
- Extended `DataPlane` API with a possibility to specify `PodDisruptionBudget` to be
created for the `DataPlane` deployments via `spec.resources.podDisruptionBudget`.
[#464](https://github.com/Kong/gateway-operator/pull/464)
Expand Down
8 changes: 7 additions & 1 deletion api/v1alpha1/kongplugin_installation_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func init() {
//+kubebuilder:subresource:status
//+kubebuilder:resource:shortName=kpi,categories=kong;all
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Ready",type=string,JSONPath=`.status.conditions[?(@.type=="Ready")].status`
//+kubebuilder:printcolumn:name="Accepted",description="The Resource is accepted",type=string,JSONPath=`.status.conditions[?(@.type=='Accepted')].status`

// KongPluginInstallation allows using a custom Kong Plugin distributed as a container image available in a registry.
// Such a plugin can be associated with GatewayConfiguration or DataPlane to be available for particular Kong Gateway
Expand Down Expand Up @@ -77,6 +77,12 @@ type KongPluginInstallationStatus struct {
//+listMapKey=type
//+kubebuilder:validation:MaxItems=8
Conditions []metav1.Condition `json:"conditions,omitempty"`

// UnderlyingConfigMapName is the name of the ConfigMap that contains the plugin's content.
// It is set when the plugin is successfully fetched and unpacked.
//
//+optional
UnderlyingConfigMapName string `json:"underlyingConfigMapName,omitempty"`
}

// The following are KongPluginInstallation specific types for
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ spec:
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .status.conditions[?(@.type=="Ready")].status
name: Ready
- description: The Resource is accepted
jsonPath: .status.conditions[?(@.type=='Accepted')].status
name: Accepted
type: string
name: v1alpha1
schema:
Expand Down Expand Up @@ -153,6 +154,11 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
underlyingConfigMapName:
description: |-
UnderlyingConfigMapName is the name of the ConfigMap that contains the plugin's content.
It is set when the plugin is successfully fetched and unpacked.
type: string
type: object
type: object
served: true
Expand Down
1 change: 1 addition & 0 deletions config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ resources:
- bases/gateway-operator.konghq.com_dataplanes.yaml
- bases/gateway-operator.konghq.com_gatewayconfigurations.yaml
- bases/gateway-operator.konghq.com_dataplanemetricsextensions.yaml
- bases/gateway-operator.konghq.com_kongplugininstallations.yaml
#+kubebuilder:scaffold:crdkustomizeresource

# patches:
Expand Down
2 changes: 2 additions & 0 deletions config/debug/manager_debug.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ spec:
- -zap-time-encoding=iso8601
- -cluster-ca-secret-namespace=kong-system
- -zap-log-level=debug
- -enable-controller-kongplugininstallation=true
- -enable-validating-webhook=true
name: manager
env:
- name: GATEWAY_OPERATOR_DEVELOPMENT_MODE
Expand Down
3 changes: 1 addition & 2 deletions config/dev/manager_dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ spec:
- -cluster-ca-secret-namespace=kong-system
- -zap-log-level=debug
- -zap-devel=true
- -enable-controller-gateway=true
- -enable-controller-controlplane=true
- -enable-controller-kongplugininstallation=true
- -enable-validating-webhook=true
name: manager
env:
Expand Down
24 changes: 24 additions & 0 deletions config/rbac/role/role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,12 @@ rules:
- patch
- update
- watch
- apiGroups:
- ""
resources:
- configmaps/status
verbs:
- get
- apiGroups:
- ""
resources:
Expand Down Expand Up @@ -451,6 +457,24 @@ rules:
- get
- list
- watch
- apiGroups:
- gateway-operator.konghq.com
resources:
- kongplugininstallations
verbs:
- get
- list
- patch
- update
- watch
- apiGroups:
- gateway-operator.konghq.com
resources:
- kongplugininstallations/status
verbs:
- get
- patch
- update
- apiGroups:
- gateway.networking.k8s.io
resources:
Expand Down
223 changes: 223 additions & 0 deletions controller/kongplugininstallation/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package kongplugininstallation

import (
"context"
"errors"
"fmt"

"github.com/samber/lo"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"oras.land/oras-go/v2/registry/remote/credentials"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
k8slog "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"

"github.com/kong/gateway-operator/api/v1alpha1"
"github.com/kong/gateway-operator/controller/kongplugininstallation/image"
"github.com/kong/gateway-operator/controller/pkg/log"
"github.com/kong/gateway-operator/pkg/utils/kubernetes"
)

// Reconciler reconciles a KongPluginInstallation object.
type Reconciler struct {
client.Client
Scheme *runtime.Scheme
DevelopmentMode bool
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&v1alpha1.KongPluginInstallation{}).
WithEventFilter(predicate.GenerationChangedPredicate{}).
Owns(&corev1.ConfigMap{}, builder.WithPredicates(
predicate.Funcs{
DeleteFunc: func(e event.DeleteEvent) bool {
return true
},
CreateFunc: func(e event.CreateEvent) bool {
return false
},
UpdateFunc: func(e event.UpdateEvent) bool {
return true
},
},
)).
Watches(
&corev1.Secret{},
handler.EnqueueRequestsFromMapFunc(r.listKongPluginInstallationsForSecret),
builder.WithPredicates(
predicate.NewPredicateFuncs(func(obj client.Object) bool {
secret, ok := obj.(*corev1.Secret)
if !ok {
return false
}
return secret.Type == corev1.SecretTypeDockerConfigJson
}),
),
).
Complete(r)
}

// Reconcile moves the current state of an object to the intended state.
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.GetLogger(ctx, "kongplugininstallation", r.DevelopmentMode)

log.Trace(logger, "reconciling KongPluginInstallation resource", req)
var kpi v1alpha1.KongPluginInstallation
if err := r.Client.Get(ctx, req.NamespacedName, &kpi); err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

log.Trace(logger, "managing KongPluginInstallation resource", kpi)
var credentialsStore credentials.Store
if kpi.Spec.ImagePullSecretRef != nil {
log.Trace(logger, "getting secret for KongPluginInstallation resource", kpi)
secretNN := client.ObjectKey{
Namespace: kpi.Spec.ImagePullSecretRef.Namespace,
Name: kpi.Spec.ImagePullSecretRef.Name,
}
if secretNN.Namespace == "" {
secretNN.Namespace = req.Namespace
}

var secret corev1.Secret
if err := r.Client.Get(
ctx,
secretNN,
&secret,
); err != nil {
return ctrl.Result{}, setStatusConditionFailedForKongPluginInstallation(ctx, r.Client, &kpi, fmt.Sprintf("cannot retrieve secret %q, because: %s", secretNN, err))
}

const requiredKey = ".dockerconfigjson"
secretData, ok := secret.Data[requiredKey]
if !ok {
return ctrl.Result{}, setStatusConditionFailedForKongPluginInstallation(
ctx, r.Client, &kpi, fmt.Sprintf("can't parse secret %q - unexpected type, it should follow 'kubernetes.io/dockerconfigjson'", secretNN),
)
}
var err error
credentialsStore, err = image.CredentialsStoreFromString(string(secretData))
if err != nil {
return ctrl.Result{}, setStatusConditionFailedForKongPluginInstallation(ctx, r.Client, &kpi, fmt.Sprintf("can't parse secret: %q data: %s", secretNN, err))
}
}

log.Trace(logger, "fetch plugin for KongPluginInstallation resource", kpi)
plugin, err := image.FetchPluginContent(ctx, kpi.Spec.Image, credentialsStore)
if err != nil {
return ctrl.Result{}, setStatusConditionFailedForKongPluginInstallation(ctx, r.Client, &kpi, fmt.Sprintf("problem with the image: %q error: %s", kpi.Spec.Image, err))
}

cms, err := kubernetes.ListConfigMapsForOwner(ctx, r.Client, kpi.GetUID())
if err != nil {
return ctrl.Result{}, err
}
var cm corev1.ConfigMap
switch len(cms) {
case 0:
if cmName := kpi.Status.UnderlyingConfigMapName; cmName != "" {
cm.Name = cmName
} else {
cm.GenerateName = kpi.Name
}
cm.Namespace = kpi.Namespace
cm.Data = map[string]string{
fmt.Sprintf("%s.lua", kpi.Name): string(plugin),
}
if err := ctrl.SetControllerReference(&kpi, &cm, r.Scheme); err != nil {
return ctrl.Result{}, err
}
if err := r.Client.Create(ctx, &cm); err != nil {
return ctrl.Result{}, err
}
kpi.Status.UnderlyingConfigMapName = cm.Name
case 1:
cm = cms[0]
cm.Data = map[string]string{
fmt.Sprintf("%s.lua", kpi.Name): string(plugin),
}
if err := r.Client.Update(ctx, &cm); err != nil {
return ctrl.Result{}, err
}
default:
// It should never happen.
return ctrl.Result{}, errors.New("unexpected error happened - more than one ConfigMap found")
}

return ctrl.Result{}, setStatusConditionForKongPluginInstallation(
ctx, r.Client, &kpi, metav1.ConditionTrue, v1alpha1.KongPluginInstallationReasonReady, "plugin successfully saved in cluster as ConfigMap",
)
}

func (r *Reconciler) listKongPluginInstallationsForSecret(ctx context.Context, obj client.Object) []reconcile.Request {
name, namespace := obj.GetName(), obj.GetNamespace()

var kpiList v1alpha1.KongPluginInstallationList
if err := r.List(ctx, &kpiList); err != nil {
k8slog.FromContext(ctx).Error(
err,
"failed to run map funcs for secrets",
)
return nil
}

var recs []reconcile.Request
for _, kpi := range kpiList.Items {
if kpi.Spec.ImagePullSecretRef == nil {
continue
}
if kpi.Spec.ImagePullSecretRef.Namespace == "" {
kpi.Spec.ImagePullSecretRef.Namespace = kpi.Namespace
}
if kpi.Spec.ImagePullSecretRef.Namespace == namespace && kpi.Spec.ImagePullSecretRef.Name == name {
recs = append(recs, reconcile.Request{
NamespacedName: client.ObjectKey{
Name: kpi.Name,
Namespace: kpi.Namespace,
},
})
}
}
return recs
}

func setStatusConditionFailedForKongPluginInstallation(
ctx context.Context, client client.Client, kpi *v1alpha1.KongPluginInstallation, msg string,
) error {
return setStatusConditionForKongPluginInstallation(ctx, client, kpi, metav1.ConditionFalse, v1alpha1.KongPluginInstallationReasonFailed, msg)
}

func setStatusConditionForKongPluginInstallation(
ctx context.Context, client client.Client, kpi *v1alpha1.KongPluginInstallation, conditionStatus metav1.ConditionStatus, reason v1alpha1.KongPluginInstallationConditionReason, msg string,
) error {
status := metav1.Condition{
Type: string(v1alpha1.KongPluginInstallationConditionStatusAccepted),
Status: conditionStatus,
ObservedGeneration: kpi.Generation,
LastTransitionTime: metav1.Now(),
Reason: string(reason),
Message: msg,
}
_, index, found := lo.FindIndexOf(kpi.Status.Conditions, func(c metav1.Condition) bool {
return c.Type == string(v1alpha1.KongPluginInstallationConditionStatusAccepted)
})
if found {
// Nothing changed, condition doesn't need to be updated.
if c := kpi.Status.Conditions[index]; c.Status == status.Status && c.Reason == status.Reason && c.Message == status.Message {
return nil
}
kpi.Status.Conditions[index] = status
} else {
kpi.Status.Conditions = append(kpi.Status.Conditions, status)
}
return client.Status().Update(ctx, kpi)
}
7 changes: 7 additions & 0 deletions controller/kongplugininstallation/controller_rbac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package kongplugininstallation

//+kubebuilder:rbac:groups=gateway-operator.konghq.com,resources=kongplugininstallations,verbs=get;list;watch;update;patch
//+kubebuilder:rbac:groups=gateway-operator.konghq.com,resources=kongplugininstallations/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;
//+kubebuilder:rbac:groups=core,resources=configmaps/status,verbs=get
Loading

0 comments on commit cdfb65a

Please sign in to comment.