diff --git a/KIC_STANDALONE_V1ALPHA1.md b/KIC_STANDALONE_V1ALPHA1.md new file mode 100644 index 0000000000..e8f1430fab --- /dev/null +++ b/KIC_STANDALONE_V1ALPHA1.md @@ -0,0 +1,299 @@ +# KIC Standalone Support for v1alpha1 Kong Gateway Entity CRDs + +## Overview + +This document describes the changes made to support the v1alpha1 Kong Gateway entity CRDs +(`KongService`, `KongRoute`, `KongUpstream`, `KongTarget`, `KongCertificate`, `KongCACertificate`, +`KongSNI`, `KongPluginBinding`) in **KIC standalone db-less mode**, without requiring a Konnect +control plane. + +### Background + +These CRDs were originally designed for the Konnect pipeline (Pipeline 2): a reconciler reads +the CR and calls the Konnect REST API via `sdk-konnect-go`. A CRD-level XValidation rule +(`self.controlPlaneRef.type != 'kic'`) explicitly blocked their use in KIC standalone mode. + +The goal of this change is to add a **KIC standalone path** (Pipeline 1) where these CRDs are +translated directly to `kong.*` types (from `github.com/kong/go-kong`) and pushed to the Kong +Admin API via `POST /config` in db-less mode, with Kubernetes as the sole source of truth. +The Konnect path remains entirely unchanged. + +--- + +## Architecture + +``` +K8s CRDs (v1alpha1) + │ + ├── [Pipeline 1 – KIC standalone, this change] + │ KIC watches CRs → Store → Translator → KongState (kong.*) → POST /config (db-less) + │ + └── [Pipeline 2 – Konnect, unchanged] + reconciler_generic reads CRs → sdk-konnect-go → Konnect REST API +``` + +The two pipelines are independent. Enabling `KongServiceV1Alpha1` feature gate activates +Pipeline 1 without affecting Pipeline 2. + +--- + +## Feature Gate + +A new opt-in feature gate controls the KIC standalone path: + +| Feature gate | Default | Description | +|---|---|---| +| `KongServiceV1Alpha1` | `false` | Enables KIC standalone translation of v1alpha1 Kong entity CRDs | + +To enable it, set the feature gate in the manager configuration: + +```yaml +feature-gates: KongServiceV1Alpha1=true +``` + +--- + +## Supported CRD Types + +| CRD (v1alpha1) | Dependency | Maps to KongState | +|---|---|---| +| `KongService` | none | `KongState.Services` | +| `KongRoute` | `serviceRef → KongService` | `Service.Routes` | +| `KongUpstream` | none | `KongState.Upstreams` | +| `KongTarget` | `upstreamRef → KongUpstream` | `Upstream.Targets` | +| `KongCertificate` | optional `secretRef → k8s Secret` | `KongState.Certificates` | +| `KongCACertificate` | optional `secretRef → k8s Secret` | `KongState.CACertificates` | +| `KongSNI` | `certificateRef → KongCertificate` | `Certificate.SNIs` | +| `KongPluginBinding` | `pluginRef → KongPlugin/KongClusterPlugin` + targets | `KongState.Plugins` | + +> `KongKey` and `KongKeySet` are out of scope (no native field in the standard go-kong KongState). + +### Certificate sourcing + +`KongCertificate` and `KongCACertificate` support two source types: + +- **`inline`**: certificate data is provided directly in the CR spec (`cert`, `key`, `certAlt`, `keyAlt`). +- **`secretRef`**: certificate data is read from a Kubernetes Secret: + - `KongCertificate`: secret must contain `tls.crt` and `tls.key` + - `KongCACertificate`: secret must contain `ca.crt` + +### Type conversion bridge + +The v1alpha1 CRD specs use types from `github.com/Kong/sdk-konnect-go/models/components` +while KongState uses types from `github.com/kong/go-kong`. Both represent the same Kong API +entities and share identical JSON field names, so complex nested types (e.g., `Healthchecks`) +are converted via a JSON marshal/unmarshal round-trip: + +```go +func convertViaJSON[F, T any](from F) (T, error) { + b, _ := json.Marshal(from) + var to T + return to, json.Unmarshal(b, &to) +} +``` + +--- + +## Files Changed + +### 1. CRD type validation — removed blocking XValidation rule + +The rule `self.controlPlaneRef.type != 'kic'` was removed from the spec of: + +| File | +|---| +| `api/configuration/v1alpha1/kongservice_types.go` | +| `api/configuration/v1alpha1/kongroute_types.go` | +| `api/configuration/v1alpha1/kongupstream_types.go` | +| `api/configuration/v1alpha1/kongcertificate_types.go` | +| `api/configuration/v1alpha1/kongcacertificate_types.go` | +| `api/configuration/v1alpha1/kongdataplaneclientcertificate_types.go` | +| `api/configuration/v1alpha1/kongkey_types.go` | +| `api/configuration/v1alpha1/kongkeyset_types.go` | + +### 2. Store generator spec + +**`hack/generators/cache-stores/spec.go`** — Added 8 new entries to `supportedTypes`: + +```go +{Type: "KongService", Package: "kongv1alpha1", StoreField: "KongServiceV1Alpha1"}, +{Type: "KongRoute", Package: "kongv1alpha1", StoreField: "KongRouteV1Alpha1"}, +{Type: "KongUpstream", Package: "kongv1alpha1", StoreField: "KongUpstreamV1Alpha1"}, +{Type: "KongTarget", Package: "kongv1alpha1", StoreField: "KongTargetV1Alpha1"}, +{Type: "KongCertificate", Package: "kongv1alpha1", StoreField: "KongCertificateV1Alpha1"}, +{Type: "KongCACertificate", Package: "kongv1alpha1", StoreField: "KongCACertificateV1Alpha1"}, +{Type: "KongSNI", Package: "kongv1alpha1", StoreField: "KongSNIV1Alpha1"}, +{Type: "KongPluginBinding", Package: "kongv1alpha1", StoreField: "KongPluginBindingV1Alpha1"}, +``` + +### 3. Generated cache store + +**`ingress-controller/internal/store/zz_generated.cache_stores.go`** — Updated manually +(generator not re-run) to add 8 new cache store fields, their initialization, `Get`/`Add`/`Delete` +switch cases, and entries in `ListAllStores()` / `SupportedTypes()`. + +### 4. Store interface and implementation + +**`ingress-controller/internal/store/store.go`** — Added 8 new methods to the `Storer` interface +and their implementations, following the `ListKongVaults()` pattern with `isValidIngressClass` +filtering: + +```go +ListKongServicesV1Alpha1() []*configurationv1alpha1.KongService +ListKongRoutesV1Alpha1() []*configurationv1alpha1.KongRoute +ListKongUpstreamsV1Alpha1() []*configurationv1alpha1.KongUpstream +ListKongTargetsV1Alpha1() []*configurationv1alpha1.KongTarget +ListKongCertificatesV1Alpha1() []*configurationv1alpha1.KongCertificate +ListKongCACertificatesV1Alpha1() []*configurationv1alpha1.KongCACertificate +ListKongSNIsV1Alpha1() []*configurationv1alpha1.KongSNI +ListKongPluginBindingsV1Alpha1() []*configurationv1alpha1.KongPluginBinding +``` + +### 5. Fake store + +**`ingress-controller/internal/store/fake_store.go`** — Added 8 new fields to `FakeObjects` +and their initialization in `NewFakeStore()`. + +### 6. Fallback dependency graph + +**`ingress-controller/internal/dataplane/fallback/graph_dependencies.go`** — Added explicit +switch cases for all 8 new types. + +Types with no dependencies return `nil`: +- `KongService`, `KongUpstream`, `KongCertificate`, `KongCACertificate` + +Types with dependencies have dedicated resolve functions: +- `KongRoute` → resolves `serviceRef.namespacedRef` → `KongService` +- `KongTarget` → resolves `upstreamRef` → `KongUpstream` +- `KongSNI` → resolves `certificateRef` → `KongCertificate` +- `KongPluginBinding` → resolves `pluginReference` → `KongPlugin`/`KongClusterPlugin` + optional targets + +### 7. Feature gate constant + +**`ingress-controller/pkg/manager/config/feature_gates_keys.go`** — Added: + +```go +const KongServiceV1Alpha1Feature = "KongServiceV1Alpha1" +// default: false (opt-in) +``` + +### 8. Translator feature flags + +**`ingress-controller/internal/dataplane/translator/translator.go`** — Added `KongServiceV1Alpha1 bool` +to `FeatureFlags` and wired it to the feature gate in `NewFeatureFlags()`. + +The `BuildKongConfig()` method now calls the three new Fill methods when the flag is set: + +```go +if t.featureFlags.KongServiceV1Alpha1 { + result.FillFromKongServicesV1Alpha1(t.logger, t.storer, t.failuresCollector) + result.FillFromKongCertificatesV1Alpha1(t.logger, t.storer, t.failuresCollector) + result.FillFromKongPluginBindingsV1Alpha1(t.logger, t.storer, t.failuresCollector) +} +``` + +### 9. KongState translation layer (new file) + +**`ingress-controller/internal/dataplane/kongstate/kongservices_v1alpha1.go`** — New file +implementing the full translation from v1alpha1 CRs to `kong.*` KongState types: + +| Method | Description | +|---|---| +| `FillFromKongServicesV1Alpha1` | Translates `KongUpstream`+`KongTarget` → `Upstream`, `KongService`+`KongRoute` → `Service` | +| `FillFromKongCertificatesV1Alpha1` | Translates `KongCertificate` → `Certificate`, `KongCACertificate` → `CACertificate`, `KongSNI` → `Certificate.SNIs` | +| `FillFromKongPluginBindingsV1Alpha1` | Translates `KongPluginBinding` → `Plugin` (with target resolution) | + +Translation order within `FillFromKongServicesV1Alpha1`: +1. `KongUpstream` → build upstream map +2. `KongTarget` → attach to upstreams by `upstreamRef` +3. `KongService` → build service map +4. `KongRoute` → attach to services by `serviceRef.namespacedRef`; serviceless routes get a placeholder `0.0.0.0:1` service + +### 10. Controller definitions + +**`ingress-controller/internal/manager/controllerdef.go`** — Added 8 new `ControllerDef` entries, +all guarded by `featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature)`. + +### 11. Generated reconcilers + +**`ingress-controller/internal/controllers/configuration/zz_generated.controllers.go`** — Added +8 new reconciler structs following the `KongV1Alpha1KongCustomEntityReconciler` pattern: + +- `KongV1Alpha1KongServiceReconciler` +- `KongV1Alpha1KongRouteReconciler` +- `KongV1Alpha1KongUpstreamReconciler` +- `KongV1Alpha1KongTargetReconciler` +- `KongV1Alpha1KongCertificateReconciler` +- `KongV1Alpha1KongCACertificateReconciler` +- `KongV1Alpha1KongSNIReconciler` +- `KongV1Alpha1KongPluginBindingReconciler` + +Each reconciler implements `SetupWithManager`, `Reconcile`, `SetLogger`, `listClassless` +and the standard KIC reconciliation loop (get → delete-on-not-found → deletion-timestamp check → +ingress-class filter → UpdateObject → status update). + +--- + +## Usage Example + +```yaml +# KongUpstream +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongUpstream +metadata: + name: my-upstream + namespace: default +spec: + name: my-upstream + algorithm: round-robin +--- +# KongService +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongService +metadata: + name: my-service + namespace: default +spec: + host: my-upstream + port: 80 + protocol: http +--- +# KongRoute +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongRoute +metadata: + name: my-route + namespace: default +spec: + serviceRef: + type: namespacedRef + namespacedRef: + name: my-service + protocols: + - http + paths: + - /api +``` + +--- + +## What Was Not Changed + +- The Konnect reconciliation pipeline (`controller/konnect/`) is untouched. +- `KongKey` and `KongKeySet` are not supported in this implementation. +- No new CRD schemas were introduced; only existing v1alpha1 CRDs are used. +- `make manifests` / `make generate` were not re-run; CRD YAML files are unchanged + (the removed XValidation rule requires a `make manifests` run before deploying updated CRDs). + +--- + +## Post-merge Steps + +Before deploying to a cluster: + +```bash +make manifests # Regenerate CRD YAML after XValidation rule removal +make generate # Regenerate deepcopy, store, docs +make test.unit # Verify unit tests pass +make test.envtest # Verify controller tests pass +``` diff --git a/api/configuration/v1alpha1/kongcacertificate_types.go b/api/configuration/v1alpha1/kongcacertificate_types.go index c036cdf164..f861d6fd16 100644 --- a/api/configuration/v1alpha1/kongcacertificate_types.go +++ b/api/configuration/v1alpha1/kongcacertificate_types.go @@ -41,7 +41,6 @@ type KongCACertificate struct { } // KongCACertificateSpec contains the specification for the KongCACertificate. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef')", message="spec.adopt is allowed only when controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" // +kubebuilder:validation:XValidation:rule="self.type != 'inline' || (has(self.cert) && self.cert.size() != 0)", message="spec.cert is required when type is 'inline'" diff --git a/api/configuration/v1alpha1/kongcertificate_types.go b/api/configuration/v1alpha1/kongcertificate_types.go index edeccb2028..b93017410f 100644 --- a/api/configuration/v1alpha1/kongcertificate_types.go +++ b/api/configuration/v1alpha1/kongcertificate_types.go @@ -41,7 +41,6 @@ type KongCertificate struct { } // KongCertificateSpec contains the specification for the KongCertificate. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef')", message="spec.adopt is allowed only when controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" // +kubebuilder:validation:XValidation:rule="self.type != 'inline' || (has(self.cert) && self.cert.size() != 0)", message="spec.cert is required when type is 'inline'" diff --git a/api/configuration/v1alpha1/kongdataplaneclientcertificate_types.go b/api/configuration/v1alpha1/kongdataplaneclientcertificate_types.go index ae9d9380bb..f6b609eb21 100644 --- a/api/configuration/v1alpha1/kongdataplaneclientcertificate_types.go +++ b/api/configuration/v1alpha1/kongdataplaneclientcertificate_types.go @@ -48,7 +48,6 @@ type KongDataPlaneClientCertificate struct { } // KongDataPlaneClientCertificateSpec defines the spec for a KongDataPlaneClientCertificate. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" type KongDataPlaneClientCertificateSpec struct { // KongDataPlaneClientCertificateAPISpec are the attributes of the KongDataPlaneClientCertificate itself. KongDataPlaneClientCertificateAPISpec `json:",inline"` diff --git a/api/configuration/v1alpha1/kongkey_types.go b/api/configuration/v1alpha1/kongkey_types.go index 7a4110c4a9..28f5785acf 100644 --- a/api/configuration/v1alpha1/kongkey_types.go +++ b/api/configuration/v1alpha1/kongkey_types.go @@ -47,7 +47,6 @@ type KongKey struct { } // KongKeySpec defines the spec for a KongKey. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef')", message="spec.adopt is allowed only when controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" type KongKeySpec struct { diff --git a/api/configuration/v1alpha1/kongkeyset_types.go b/api/configuration/v1alpha1/kongkeyset_types.go index 486ba8f2e9..e46a5d16aa 100644 --- a/api/configuration/v1alpha1/kongkeyset_types.go +++ b/api/configuration/v1alpha1/kongkeyset_types.go @@ -46,7 +46,6 @@ type KongKeySet struct { } // KongKeySetSpec defines the spec for a KongKeySet. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef')", message="spec.adopt is allowed only when controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" type KongKeySetSpec struct { diff --git a/api/configuration/v1alpha1/kongroute_types.go b/api/configuration/v1alpha1/kongroute_types.go index c1cb15d6b6..8217abdda0 100644 --- a/api/configuration/v1alpha1/kongroute_types.go +++ b/api/configuration/v1alpha1/kongroute_types.go @@ -51,7 +51,6 @@ type KongRoute struct { // KongRouteSpec defines spec of a Kong Route. // -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.serviceRef) || (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef'))", message="spec.adopt is allowed only when serviceRef exists or controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" type KongRouteSpec struct { diff --git a/api/configuration/v1alpha1/kongservice_types.go b/api/configuration/v1alpha1/kongservice_types.go index 43d1638860..a1bef7305c 100644 --- a/api/configuration/v1alpha1/kongservice_types.go +++ b/api/configuration/v1alpha1/kongservice_types.go @@ -50,7 +50,6 @@ type KongService struct { } // KongServiceSpec defines specification of a Kong Service. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" // +kubebuilder:validation:XValidation:rule="!has(self.adopt) ? true : (has(self.controlPlaneRef) && self.controlPlaneRef.type == 'konnectNamespacedRef')", message="spec.adopt is allowed only when controlPlaneRef is konnectNamespacedRef" // +kubebuilder:validation:XValidation:rule="(has(oldSelf.adopt) && has(self.adopt)) || (!has(oldSelf.adopt) && !has(self.adopt))", message="Cannot set or unset spec.adopt in updates" type KongServiceSpec struct { diff --git a/api/configuration/v1alpha1/kongupstream_types.go b/api/configuration/v1alpha1/kongupstream_types.go index a43f1a9c59..8d37eb6c70 100644 --- a/api/configuration/v1alpha1/kongupstream_types.go +++ b/api/configuration/v1alpha1/kongupstream_types.go @@ -48,7 +48,6 @@ type KongUpstream struct { } // KongUpstreamSpec defines the spec of Kong Upstream. -// +kubebuilder:validation:XValidation:rule="!has(self.controlPlaneRef) ? true : self.controlPlaneRef.type != 'kic'", message="KIC is not supported as control plane" type KongUpstreamSpec struct { KongUpstreamAPISpec `json:",inline"` diff --git a/hack/generators/cache-stores/spec.go b/hack/generators/cache-stores/spec.go index 096a79bdaf..3433512299 100644 --- a/hack/generators/cache-stores/spec.go +++ b/hack/generators/cache-stores/spec.go @@ -108,4 +108,45 @@ var supportedTypes = []cacheStoreSupportedType{ Type: "KongCustomEntity", Package: "kongv1alpha1", }, + // v1alpha1 Kong Gateway entity types (KIC standalone support) + { + Type: "KongService", + Package: "kongv1alpha1", + StoreField: "KongServiceV1Alpha1", + }, + { + Type: "KongRoute", + Package: "kongv1alpha1", + StoreField: "KongRouteV1Alpha1", + }, + { + Type: "KongUpstream", + Package: "kongv1alpha1", + StoreField: "KongUpstreamV1Alpha1", + }, + { + Type: "KongTarget", + Package: "kongv1alpha1", + StoreField: "KongTargetV1Alpha1", + }, + { + Type: "KongCertificate", + Package: "kongv1alpha1", + StoreField: "KongCertificateV1Alpha1", + }, + { + Type: "KongCACertificate", + Package: "kongv1alpha1", + StoreField: "KongCACertificateV1Alpha1", + }, + { + Type: "KongSNI", + Package: "kongv1alpha1", + StoreField: "KongSNIV1Alpha1", + }, + { + Type: "KongPluginBinding", + Package: "kongv1alpha1", + StoreField: "KongPluginBindingV1Alpha1", + }, } diff --git a/ingress-controller/internal/controllers/configuration/zz_generated.controllers.go b/ingress-controller/internal/controllers/configuration/zz_generated.controllers.go index d3d294107e..752a8a0447 100644 --- a/ingress-controller/internal/controllers/configuration/zz_generated.controllers.go +++ b/ingress-controller/internal/controllers/configuration/zz_generated.controllers.go @@ -1761,6 +1761,905 @@ func (r *KongV1Alpha1KongCustomEntityReconciler) Reconcile(ctx context.Context, return ctrl.Result{}, nil } +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongService - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongServiceReconciler reconciles KongService resources +type KongV1Alpha1KongServiceReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongServiceReconciler{} + +// SetupWithManager sets up the controller with the Manager. +func (r *KongV1Alpha1KongServiceReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongService"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { + return r.Log + }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource( + source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Group: "configuration.konghq.com", + Version: "v1alpha1", + Kind: "KongService", + }), + &handler.EnqueueRequestForObject{}, + ), + ) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, + handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates( + predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass), + ), + ) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongService{}, + &handler.EnqueueRequestForObject{}, + builder.WithPredicates(preds), + ).Complete(r) +} + +func (r *KongV1Alpha1KongServiceReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongServiceList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongservices") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{ + NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}, + }) + } + } + return recs +} + +func (r *KongV1Alpha1KongServiceReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongservices,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongservices/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongService", req.NamespacedName) + obj := new(kongv1alpha1.KongService) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongService", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + log.V(logging.DebugLevel).Info("Object missing ingress class, ensuring it's removed from configuration", "namespace", req.Namespace, "name", req.Name, "class", r.IngressClassName) + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongRoute - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongRouteReconciler reconciles KongRoute resources +type KongV1Alpha1KongRouteReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongRouteReconciler{} + +func (r *KongV1Alpha1KongRouteReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongRoute"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongRoute"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongRoute{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongRouteReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongRouteList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongroutes") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongRouteReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongRoute", req.NamespacedName) + obj := new(kongv1alpha1.KongRoute) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongRoute", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongUpstream - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongUpstreamReconciler reconciles KongUpstream resources +type KongV1Alpha1KongUpstreamReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongUpstreamReconciler{} + +func (r *KongV1Alpha1KongUpstreamReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongUpstream"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongUpstream"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongUpstream{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongUpstreamReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongUpstreamList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongupstreams") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongUpstreamReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongupstreams,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongupstreams/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongUpstreamReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongUpstream", req.NamespacedName) + obj := new(kongv1alpha1.KongUpstream) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongUpstream", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongTarget - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongTargetReconciler reconciles KongTarget resources +type KongV1Alpha1KongTargetReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongTargetReconciler{} + +func (r *KongV1Alpha1KongTargetReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongTarget"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongTarget"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongTarget{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongTargetReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongTargetList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongtargets") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongTargetReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongtargets,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongtargets/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongTargetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongTarget", req.NamespacedName) + obj := new(kongv1alpha1.KongTarget) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongTarget", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongCertificate - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongCertificateReconciler reconciles KongCertificate resources +type KongV1Alpha1KongCertificateReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongCertificateReconciler{} + +func (r *KongV1Alpha1KongCertificateReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongCertificate"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongCertificate"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongCertificate{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongCertificateReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongCertificateList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongcertificates") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongCertificateReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcertificates,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcertificates/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongCertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongCertificate", req.NamespacedName) + obj := new(kongv1alpha1.KongCertificate) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongCertificate", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongCACertificate - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongCACertificateReconciler reconciles KongCACertificate resources +type KongV1Alpha1KongCACertificateReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongCACertificateReconciler{} + +func (r *KongV1Alpha1KongCACertificateReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongCACertificate"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongCACertificate"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongCACertificate{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongCACertificateReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongCACertificateList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongcacertificates") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongCACertificateReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcacertificates,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongcacertificates/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongCACertificateReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongCACertificate", req.NamespacedName) + obj := new(kongv1alpha1.KongCACertificate) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongCACertificate", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongSNI - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongSNIReconciler reconciles KongSNI resources +type KongV1Alpha1KongSNIReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongSNIReconciler{} + +func (r *KongV1Alpha1KongSNIReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongSNI"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongSNI"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongSNI{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongSNIReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongSNIList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongsnies") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongSNIReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongsnies,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongsnies/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongSNIReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongSNI", req.NamespacedName) + obj := new(kongv1alpha1.KongSNI) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongSNI", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongPluginBinding - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongPluginBindingReconciler reconciles KongPluginBinding resources +type KongV1Alpha1KongPluginBindingReconciler struct { + client.Client + + Log logr.Logger + Scheme *runtime.Scheme + DataplaneClient controllers.DataPlane + CacheSyncTimeout time.Duration + StatusQueue *status.Queue + + IngressClassName string + DisableIngressClassLookups bool +} + +var _ controllers.Reconciler = &KongV1Alpha1KongPluginBindingReconciler{} + +func (r *KongV1Alpha1KongPluginBindingReconciler) SetupWithManager(mgr ctrl.Manager) error { + blder := ctrl.NewControllerManagedBy(mgr). + Named("KongV1Alpha1KongPluginBinding"). + WithOptions(controller.Options{ + LogConstructor: func(_ *reconcile.Request) logr.Logger { return r.Log }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if r.StatusQueue != nil { + blder.WatchesRawSource(source.Channel( + r.StatusQueue.Subscribe(schema.GroupVersionKind{Group: "configuration.konghq.com", Version: "v1alpha1", Kind: "KongPluginBinding"}), + &handler.EnqueueRequestForObject{}, + )) + } + if !r.DisableIngressClassLookups { + blder.Watches(&netv1.IngressClass{}, handler.EnqueueRequestsFromMapFunc(r.listClassless), + builder.WithPredicates(predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass))) + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return blder.Watches(&kongv1alpha1.KongPluginBinding{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(preds)).Complete(r) +} + +func (r *KongV1Alpha1KongPluginBindingReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongPluginBindingList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongpluginbindings") + return nil + } + var recs []reconcile.Request + for i, resource := range resourceList.Items { + if ctrlutils.IsIngressClassEmpty(&resourceList.Items[i]) { + recs = append(recs, reconcile.Request{NamespacedName: k8stypes.NamespacedName{Namespace: resource.Namespace, Name: resource.Name}}) + } + } + return recs +} + +func (r *KongV1Alpha1KongPluginBindingReconciler) SetLogger(l logr.Logger) { r.Log = l } + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongpluginbindings,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongpluginbindings/status,verbs=get;update;patch + +func (r *KongV1Alpha1KongPluginBindingReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongPluginBinding", req.NamespacedName) + obj := new(kongv1alpha1.KongPluginBinding) + if err := r.Get(ctx, req.NamespacedName, obj); err != nil { + if apierrors.IsNotFound(err) { + obj.Namespace = req.Namespace + obj.Name = req.Name + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + return ctrl.Result{}, err + } + log.V(logging.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(logging.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongPluginBinding", "namespace", req.Namespace, "name", req.Name) + objectExistsInCache, err := r.DataplaneClient.ObjectExists(obj) + if err != nil { + return ctrl.Result{}, err + } + if objectExistsInCache { + if err := r.DataplaneClient.DeleteObject(obj); err != nil { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, nil + } + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + log.V(logging.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + return ctrl.Result{}, r.DataplaneClient.DeleteObject(obj) + } + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + configurationStatus := r.DataplaneClient.KubernetesObjectConfigurationStatus(obj) + conditions, updateNeeded := ctrlutils.EnsureProgrammedCondition(configurationStatus, obj.Generation, obj.Status.Conditions) + obj.Status.Conditions = conditions + if updateNeeded { + return ctrl.Result{}, r.Status().Update(ctx, obj) + } + } + return ctrl.Result{}, nil +} + // ----------------------------------------------------------------------------- // API Group "" resource nodes // ----------------------------------------------------------------------------- diff --git a/ingress-controller/internal/dataplane/fallback/graph_dependencies.go b/ingress-controller/internal/dataplane/fallback/graph_dependencies.go index 162dff6028..2f1f2ce413 100644 --- a/ingress-controller/internal/dataplane/fallback/graph_dependencies.go +++ b/ingress-controller/internal/dataplane/fallback/graph_dependencies.go @@ -50,6 +50,15 @@ func ResolveDependencies(cache store.CacheStores, obj client.Object) ([]client.O return resolveKongServiceFacadeDependencies(cache, obj), nil case *configurationv1alpha1.KongCustomEntity: return resolveKongCustomEntityDependencies(cache, obj), nil + // v1alpha1 types with dependencies. + case *configurationv1alpha1.KongRoute: + return resolveKongRouteV1Alpha1Dependencies(cache, obj), nil + case *configurationv1alpha1.KongTarget: + return resolveKongTargetV1Alpha1Dependencies(cache, obj), nil + case *configurationv1alpha1.KongSNI: + return resolveKongSNIV1Alpha1Dependencies(cache, obj), nil + case *configurationv1alpha1.KongPluginBinding: + return resolveKongPluginBindingV1Alpha1Dependencies(cache, obj), nil // Object types that have no dependencies. case *netv1.IngressClass, *corev1.Secret, @@ -60,9 +69,86 @@ func ResolveDependencies(cache store.CacheStores, obj client.Object) ([]client.O *gatewayapi.BackendTLSPolicy, *configurationv1beta1.KongUpstreamPolicy, *configurationv1alpha1.IngressClassParameters, - *configurationv1alpha1.KongVault: + *configurationv1alpha1.KongVault, + *configurationv1alpha1.KongService, + *configurationv1alpha1.KongUpstream, + *configurationv1alpha1.KongCertificate, + *configurationv1alpha1.KongCACertificate: return nil, nil default: return nil, fmt.Errorf("unsupported object type: %T", obj) } } + +func resolveKongRouteV1Alpha1Dependencies(cache store.CacheStores, obj *configurationv1alpha1.KongRoute) []client.Object { + if obj.Spec.ServiceRef == nil || obj.Spec.ServiceRef.NamespacedRef == nil { + return nil + } + svc, exists, err := cache.KongServiceV1Alpha1.GetByKey(fmt.Sprintf("%s/%s", obj.Namespace, obj.Spec.ServiceRef.NamespacedRef.Name)) + if err != nil || !exists { + return nil + } + kongSvc, ok := svc.(*configurationv1alpha1.KongService) + if !ok { + return nil + } + return []client.Object{kongSvc} +} + +func resolveKongTargetV1Alpha1Dependencies(cache store.CacheStores, obj *configurationv1alpha1.KongTarget) []client.Object { + upstream, exists, err := cache.KongUpstreamV1Alpha1.GetByKey(fmt.Sprintf("%s/%s", obj.Namespace, obj.Spec.UpstreamRef.Name)) + if err != nil || !exists { + return nil + } + kongUpstream, ok := upstream.(*configurationv1alpha1.KongUpstream) + if !ok { + return nil + } + return []client.Object{kongUpstream} +} + +func resolveKongSNIV1Alpha1Dependencies(cache store.CacheStores, obj *configurationv1alpha1.KongSNI) []client.Object { + cert, exists, err := cache.KongCertificateV1Alpha1.GetByKey(fmt.Sprintf("%s/%s", obj.Namespace, obj.Spec.CertificateRef.Name)) + if err != nil || !exists { + return nil + } + kongCert, ok := cert.(*configurationv1alpha1.KongCertificate) + if !ok { + return nil + } + return []client.Object{kongCert} +} + +func resolveKongPluginBindingV1Alpha1Dependencies(cache store.CacheStores, obj *configurationv1alpha1.KongPluginBinding) []client.Object { + var deps []client.Object + // Resolve plugin reference. + pluginNS := obj.Namespace + if obj.Spec.PluginReference.Namespace != "" { + pluginNS = obj.Spec.PluginReference.Namespace + } + pluginKey := fmt.Sprintf("%s/%s", pluginNS, obj.Spec.PluginReference.Name) + if plugin, exists, err := cache.Plugin.GetByKey(pluginKey); err == nil && exists { + if p, ok := plugin.(client.Object); ok { + deps = append(deps, p) + } + } + if obj.Spec.Targets == nil { + return deps + } + // Resolve target references. + if obj.Spec.Targets.ServiceReference != nil && obj.Spec.Targets.ServiceReference.Kind == "KongService" { + if svc, exists, err := cache.KongServiceV1Alpha1.GetByKey(fmt.Sprintf("%s/%s", obj.Namespace, obj.Spec.Targets.ServiceReference.Name)); err == nil && exists { + if s, ok := svc.(client.Object); ok { + deps = append(deps, s) + } + } + } + if obj.Spec.Targets.RouteReference != nil && obj.Spec.Targets.RouteReference.Kind == "KongRoute" { + if route, exists, err := cache.KongRouteV1Alpha1.GetByKey(fmt.Sprintf("%s/%s", obj.Namespace, obj.Spec.Targets.RouteReference.Name)); err == nil && exists { + if r, ok := route.(client.Object); ok { + deps = append(deps, r) + } + } + } + return deps +} diff --git a/ingress-controller/internal/dataplane/kongstate/kongservices_v1alpha1.go b/ingress-controller/internal/dataplane/kongstate/kongservices_v1alpha1.go new file mode 100644 index 0000000000..8dd0d7a6ae --- /dev/null +++ b/ingress-controller/internal/dataplane/kongstate/kongservices_v1alpha1.go @@ -0,0 +1,574 @@ +package kongstate + +// KIC standalone support for v1alpha1 Kong Gateway entity CRDs. +// +// This file implements translation from api/configuration/v1alpha1 types +// (KongService, KongRoute, KongUpstream, KongTarget, KongCertificate, +// KongCACertificate, KongSNI, KongPluginBinding) to the kong.* types +// used in KongState, enabling db-less mode without a Konnect control plane. +// +// The Konnect reconciliation path (sdk-konnect-go) is untouched. +// Enabled by feature gate: KongServiceV1Alpha1. + +import ( + "encoding/json" + "fmt" + + "github.com/go-logr/logr" + "github.com/kong/go-kong/kong" + + configurationv1alpha1 "github.com/kong/kong-operator/v2/api/configuration/v1alpha1" + "github.com/kong/kong-operator/v2/ingress-controller/internal/dataplane/failures" + "github.com/kong/kong-operator/v2/ingress-controller/internal/store" + "github.com/kong/kong-operator/v2/ingress-controller/internal/util" +) + +// FillFromKongServicesV1Alpha1 translates KongService, KongRoute, KongUpstream, +// and KongTarget CRDs from the store into KongState (kong.* types) for KIC standalone. +func (ks *KongState) FillFromKongServicesV1Alpha1( + logger logr.Logger, + s store.Storer, + failuresCollector *failures.ResourceFailuresCollector, +) { + // --- KongUpstream → Upstream --- + // Build a map of upstream name → kongstate.Upstream for later KongTarget attachment. + upstreamByName := map[string]*Upstream{} + for _, ku := range s.ListKongUpstreamsV1Alpha1() { + name := ku.Spec.Name + if name == "" { + name = ku.Name + } + u := translateKongUpstreamV1Alpha1(ku) + ks.Upstreams = append(ks.Upstreams, u) + upstreamByName[name] = &ks.Upstreams[len(ks.Upstreams)-1] + } + + // --- KongTarget → Upstream.Targets --- + for _, kt := range s.ListKongTargetsV1Alpha1() { + upstreamName := kt.Spec.UpstreamRef.Name + upstream, ok := upstreamByName[upstreamName] + if !ok { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongTarget %q: referenced KongUpstream %q not found", kt.Name, upstreamName), + kt, + ) + continue + } + target := kong.Target{ + Target: kong.String(kt.Spec.Target), + Weight: kong.Int(kt.Spec.Weight), + Tags: util.GenerateTagsForObject(kt), + } + upstream.Targets = append(upstream.Targets, Target{Target: target}) + } + + // --- KongService → Service --- + // Build a map of k8s name → kongstate.Service for later KongRoute attachment. + svcByName := map[string]*Service{} + for _, ks2 := range s.ListKongServicesV1Alpha1() { + svc := translateKongServiceV1Alpha1(ks2) + ks.Services = append(ks.Services, svc) + svcByName[ks2.Name] = &ks.Services[len(ks.Services)-1] + } + + // --- KongRoute → Service.Routes --- + for _, kr := range s.ListKongRoutesV1Alpha1() { + route := translateKongRouteV1Alpha1(kr) + if kr.Spec.ServiceRef != nil && kr.Spec.ServiceRef.NamespacedRef != nil { + // Route attached to a specific KongService. + svcName := kr.Spec.ServiceRef.NamespacedRef.Name + svc, ok := svcByName[svcName] + if !ok { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongRoute %q: referenced KongService %q not found", kr.Name, svcName), + kr, + ) + continue + } + svc.Routes = append(svc.Routes, route) + } else { + // Serviceless route: wrap in a placeholder service. + placeholder := Service{ + Service: kong.Service{ + Name: kong.String("serviceless." + kr.Namespace + "." + kr.Name), + Host: kong.String("0.0.0.0"), + Port: kong.Int(1), + // Kong requires a valid protocol even for serviceless routes. + Protocol: kong.String("http"), + }, + Parent: kr, + Routes: []Route{route}, + } + ks.Services = append(ks.Services, placeholder) + } + } +} + +// FillFromKongCertificatesV1Alpha1 translates KongCertificate, KongCACertificate, +// and KongSNI CRDs from the store into KongState for KIC standalone. +func (ks *KongState) FillFromKongCertificatesV1Alpha1( + logger logr.Logger, + s store.Storer, + failuresCollector *failures.ResourceFailuresCollector, +) { + // --- KongCertificate → Certificate --- + // Build a map of k8s name → index in ks.Certificates for SNI attachment. + certIndexByName := map[string]int{} + for _, kc := range s.ListKongCertificatesV1Alpha1() { + cert, err := translateKongCertificateV1Alpha1(kc, s) + if err != nil { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongCertificate %q: %v", kc.Name, err), + kc, + ) + continue + } + certIndexByName[kc.Name] = len(ks.Certificates) + ks.Certificates = append(ks.Certificates, Certificate{Certificate: cert}) + } + + // --- KongCACertificate → CACertificates --- + for _, kca := range s.ListKongCACertificatesV1Alpha1() { + caCert, err := translateKongCACertificateV1Alpha1(kca, s) + if err != nil { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongCACertificate %q: %v", kca.Name, err), + kca, + ) + continue + } + ks.CACertificates = append(ks.CACertificates, caCert) + } + + // --- KongSNI → Certificate.SNIs --- + for _, ksni := range s.ListKongSNIsV1Alpha1() { + certName := ksni.Spec.CertificateRef.Name + idx, ok := certIndexByName[certName] + if !ok { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongSNI %q: referenced KongCertificate %q not found", ksni.Name, certName), + ksni, + ) + continue + } + sniName := ksni.Spec.Name + ks.Certificates[idx].SNIs = append(ks.Certificates[idx].SNIs, kong.String(sniName)) + } +} + +// FillFromKongPluginBindingsV1Alpha1 translates KongPluginBinding CRDs from the +// store into KongState.Plugins for KIC standalone. +func (ks *KongState) FillFromKongPluginBindingsV1Alpha1( + logger logr.Logger, + s store.Storer, + failuresCollector *failures.ResourceFailuresCollector, +) { + for _, kpb := range s.ListKongPluginBindingsV1Alpha1() { + plugins, err := translateKongPluginBindingV1Alpha1(kpb, s, logger) + if err != nil { + failuresCollector.PushResourceFailure( + fmt.Sprintf("KongPluginBinding %q: %v", kpb.Name, err), + kpb, + ) + continue + } + for _, p := range plugins { + ks.Plugins = append(ks.Plugins, Plugin{ + Plugin: p, + K8sParent: kpb, + }) + } + } +} + +// --------------------------------------------------------------------------- +// Translation helpers +// --------------------------------------------------------------------------- + +func translateKongServiceV1Alpha1(ks *configurationv1alpha1.KongService) Service { + spec := ks.Spec.KongServiceAPISpec + name := ks.Name + if spec.Name != nil { + name = *spec.Name + } + svc := kong.Service{ + Name: kong.String(name), + Host: kong.String(spec.Host), + Tags: tagsFromV1Alpha1(spec.Tags, util.GenerateTagsForObject(ks)), + } + if spec.Port != 0 { + p := int(spec.Port) + svc.Port = &p + } + if spec.Path != nil { + svc.Path = spec.Path + } + if spec.Protocol != "" { + svc.Protocol = kong.String(string(spec.Protocol)) + } + if spec.ConnectTimeout != nil { + v := int(*spec.ConnectTimeout) + svc.ConnectTimeout = &v + } + if spec.ReadTimeout != nil { + v := int(*spec.ReadTimeout) + svc.ReadTimeout = &v + } + if spec.WriteTimeout != nil { + v := int(*spec.WriteTimeout) + svc.WriteTimeout = &v + } + if spec.Retries != nil { + v := int(*spec.Retries) + svc.Retries = &v + } + if spec.Enabled != nil { + svc.Enabled = spec.Enabled + } + if spec.TLSVerify != nil { + svc.TLSVerify = spec.TLSVerify + } + if spec.TLSVerifyDepth != nil { + v := int(*spec.TLSVerifyDepth) + svc.TLSVerifyDepth = &v + } + return Service{ + Service: svc, + Parent: ks, + } +} + +func translateKongRouteV1Alpha1(kr *configurationv1alpha1.KongRoute) Route { + spec := kr.Spec.KongRouteAPISpec + name := kr.Name + if spec.Name != nil { + name = *spec.Name + } + route := kong.Route{ + Name: kong.String(name), + Hosts: stringSliceToKongStringSlice(spec.Hosts), + Paths: stringSliceToKongStringSlice(spec.Paths), + Tags: tagsFromV1Alpha1(spec.Tags, util.GenerateTagsForObject(kr)), + } + if spec.Methods != nil { + route.Methods = stringSliceToKongStringSlice(spec.Methods) + } + if spec.StripPath != nil { + route.StripPath = spec.StripPath + } + if spec.PreserveHost != nil { + route.PreserveHost = spec.PreserveHost + } + if spec.RegexPriority != nil { + v := int(*spec.RegexPriority) + route.RegexPriority = &v + } + if spec.RequestBuffering != nil { + route.RequestBuffering = spec.RequestBuffering + } + if spec.ResponseBuffering != nil { + route.ResponseBuffering = spec.ResponseBuffering + } + if spec.PathHandling != nil { + route.PathHandling = kong.String(string(*spec.PathHandling)) + } + if spec.HTTPSRedirectStatusCode != nil { + v := int(*spec.HTTPSRedirectStatusCode) + route.HTTPSRedirectStatusCode = &v + } + // Protocols: []sdkkonnectcomp.RouteJSONProtocols → []*string + if len(spec.Protocols) > 0 { + for _, p := range spec.Protocols { + ps := string(p) + route.Protocols = append(route.Protocols, &ps) + } + } + // Snis + if len(spec.Snis) > 0 { + route.SNIs = stringSliceToKongStringSlice(spec.Snis) + } + // Headers: map[string][]string → map[string][]string (same) + if len(spec.Headers) > 0 { + route.Headers = spec.Headers + } + // Sources/Destinations: use JSON bridge for struct conversion. + if len(spec.Sources) > 0 { + _ = convertViaJSON(spec.Sources, &route.Sources) + } + if len(spec.Destinations) > 0 { + _ = convertViaJSON(spec.Destinations, &route.Destinations) + } + return Route{Route: route} +} + +func translateKongUpstreamV1Alpha1(ku *configurationv1alpha1.KongUpstream) Upstream { + spec := ku.Spec.KongUpstreamAPISpec + name := ku.Spec.Name + if name == "" { + name = ku.Name + } + u := kong.Upstream{ + Name: kong.String(name), + Tags: tagsFromV1Alpha1(spec.Tags, util.GenerateTagsForObject(ku)), + } + if spec.Algorithm != nil { + u.Algorithm = kong.String(string(*spec.Algorithm)) + } + if spec.Slots != nil { + v := int(*spec.Slots) + u.Slots = &v + } + if spec.HashOn != nil { + u.HashOn = kong.String(string(*spec.HashOn)) + } + if spec.HashFallback != nil { + u.HashFallback = kong.String(string(*spec.HashFallback)) + } + if spec.HashOnHeader != nil { + u.HashOnHeader = spec.HashOnHeader + } + if spec.HashFallbackHeader != nil { + u.HashFallbackHeader = spec.HashFallbackHeader + } + if spec.HashOnCookie != nil { + u.HashOnCookie = spec.HashOnCookie + } + if spec.HashOnCookiePath != nil { + u.HashOnCookiePath = spec.HashOnCookiePath + } + if spec.HashOnQueryArg != nil { + u.HashOnQueryArg = spec.HashOnQueryArg + } + if spec.HashFallbackQueryArg != nil { + u.HashFallbackQueryArg = spec.HashFallbackQueryArg + } + if spec.HashOnURICapture != nil { + u.HashOnURICapture = spec.HashOnURICapture + } + if spec.HashFallbackURICapture != nil { + u.HashFallbackURICapture = spec.HashFallbackURICapture + } + if spec.HostHeader != nil { + u.HostHeader = spec.HostHeader + } + if spec.UseSrvName != nil { + u.UseSrvName = spec.UseSrvName + } + // Healthchecks: sdkkonnectcomp.Healthchecks → kong.Healthcheck via JSON round-trip. + if spec.Healthchecks != nil { + var hc kong.Healthcheck + if err := convertViaJSON(spec.Healthchecks, &hc); err == nil { + u.Healthchecks = &hc + } + } + return Upstream{Upstream: u} +} + +func translateKongCertificateV1Alpha1(kc *configurationv1alpha1.KongCertificate, s store.Storer) (kong.Certificate, error) { + cert := kong.Certificate{ + Tags: tagsFromV1Alpha1(kc.Spec.Tags, util.GenerateTagsForObject(kc)), + } + sourceType := configurationv1alpha1.KongCertificateSourceTypeInline + if kc.Spec.Type != nil { + sourceType = *kc.Spec.Type + } + switch sourceType { + case configurationv1alpha1.KongCertificateSourceTypeInline: + cert.Cert = kong.String(kc.Spec.Cert) + cert.Key = kong.String(kc.Spec.Key) + if kc.Spec.CertAlt != "" { + cert.CertAlt = kong.String(kc.Spec.CertAlt) + } + if kc.Spec.KeyAlt != "" { + cert.KeyAlt = kong.String(kc.Spec.KeyAlt) + } + case configurationv1alpha1.KongCertificateSourceTypeSecretRef: + if kc.Spec.SecretRef == nil { + return cert, fmt.Errorf("secretRef is required when type is 'secretRef'") + } + ns := kc.Namespace + if kc.Spec.SecretRef.Namespace != nil && *kc.Spec.SecretRef.Namespace != "" { + ns = *kc.Spec.SecretRef.Namespace + } + secret, err := s.GetSecret(ns, kc.Spec.SecretRef.Name) + if err != nil { + return cert, fmt.Errorf("failed to get secret %s/%s: %w", ns, kc.Spec.SecretRef.Name, err) + } + tlsCrt, ok := secret.Data["tls.crt"] + if !ok { + return cert, fmt.Errorf("secret %s/%s missing key tls.crt", ns, kc.Spec.SecretRef.Name) + } + tlsKey, ok := secret.Data["tls.key"] + if !ok { + return cert, fmt.Errorf("secret %s/%s missing key tls.key", ns, kc.Spec.SecretRef.Name) + } + cert.Cert = kong.String(string(tlsCrt)) + cert.Key = kong.String(string(tlsKey)) + // Alt cert from optional SecretRefAlt. + if kc.Spec.SecretRefAlt != nil { + altNS := kc.Namespace + if kc.Spec.SecretRefAlt.Namespace != nil && *kc.Spec.SecretRefAlt.Namespace != "" { + altNS = *kc.Spec.SecretRefAlt.Namespace + } + altSecret, err := s.GetSecret(altNS, kc.Spec.SecretRefAlt.Name) + if err == nil { + if v, ok := altSecret.Data["tls.crt"]; ok { + cert.CertAlt = kong.String(string(v)) + } + if v, ok := altSecret.Data["tls.key"]; ok { + cert.KeyAlt = kong.String(string(v)) + } + } + } + } + return cert, nil +} + +func translateKongCACertificateV1Alpha1(kca *configurationv1alpha1.KongCACertificate, s store.Storer) (kong.CACertificate, error) { + caCert := kong.CACertificate{ + Tags: tagsFromV1Alpha1(kca.Spec.Tags, util.GenerateTagsForObject(kca)), + } + sourceType := configurationv1alpha1.KongCACertificateSourceTypeInline + if kca.Spec.Type != nil { + sourceType = *kca.Spec.Type + } + switch sourceType { + case configurationv1alpha1.KongCACertificateSourceTypeInline: + caCert.Cert = kong.String(kca.Spec.Cert) + case configurationv1alpha1.KongCACertificateSourceTypeSecretRef: + if kca.Spec.SecretRef == nil { + return caCert, fmt.Errorf("secretRef is required when type is 'secretRef'") + } + ns := kca.Namespace + if kca.Spec.SecretRef.Namespace != nil && *kca.Spec.SecretRef.Namespace != "" { + ns = *kca.Spec.SecretRef.Namespace + } + secret, err := s.GetSecret(ns, kca.Spec.SecretRef.Name) + if err != nil { + return caCert, fmt.Errorf("failed to get secret %s/%s: %w", ns, kca.Spec.SecretRef.Name, err) + } + tlsCrt, ok := secret.Data["ca.crt"] + if !ok { + return caCert, fmt.Errorf("secret %s/%s missing key ca.crt", ns, kca.Spec.SecretRef.Name) + } + caCert.Cert = kong.String(string(tlsCrt)) + } + return caCert, nil +} + +func translateKongPluginBindingV1Alpha1( + kpb *configurationv1alpha1.KongPluginBinding, + s store.Storer, + _ logr.Logger, +) ([]kong.Plugin, error) { + // Lookup the plugin definition. + pluginRef := kpb.Spec.PluginReference + kind := "KongPlugin" + if pluginRef.Kind != nil { + kind = *pluginRef.Kind + } + + var pluginConfig kong.Configuration + var pluginName string + + switch kind { + case "KongPlugin": + ns := kpb.Namespace + if pluginRef.Namespace != "" { + ns = pluginRef.Namespace + } + kp, err := s.GetKongPlugin(ns, pluginRef.Name) + if err != nil { + return nil, fmt.Errorf("KongPlugin %s/%s not found: %w", ns, pluginRef.Name, err) + } + pluginName = kp.PluginName + pluginConfig, err = RawConfigToConfiguration(kp.Config.Raw) + if err != nil { + return nil, fmt.Errorf("KongPlugin %s/%s config parse error: %w", ns, pluginRef.Name, err) + } + case "KongClusterPlugin": + kcp, err := s.GetKongClusterPlugin(pluginRef.Name) + if err != nil { + return nil, fmt.Errorf("KongClusterPlugin %s not found: %w", pluginRef.Name, err) + } + pluginName = kcp.PluginName + pluginConfig, err = RawConfigToConfiguration(kcp.Config.Raw) + if err != nil { + return nil, fmt.Errorf("KongClusterPlugin %s config parse error: %w", pluginRef.Name, err) + } + default: + return nil, fmt.Errorf("unsupported plugin kind: %s", kind) + } + + p := kong.Plugin{ + Name: kong.String(pluginName), + Config: pluginConfig, + Tags: util.GenerateTagsForObject(kpb), + } + + if kpb.Spec.Scope == configurationv1alpha1.KongPluginBindingScopeGlobalInControlPlane { + // Global plugin — no target associations. + return []kong.Plugin{p}, nil + } + + // Target associations: we generate one plugin entry per target combination. + // For KIC db-less, the plugin must reference the service/route by name. + if kpb.Spec.Targets == nil { + return []kong.Plugin{p}, nil + } + targets := kpb.Spec.Targets + + // Build association object references. + if targets.ServiceReference != nil { + svcName := targets.ServiceReference.Name + p.Service = &kong.Service{Name: kong.String(svcName)} + } + if targets.RouteReference != nil { + routeName := targets.RouteReference.Name + p.Route = &kong.Route{Name: kong.String(routeName)} + } + if targets.ConsumerReference != nil { + p.Consumer = &kong.Consumer{Username: kong.String(targets.ConsumerReference.Name)} + } + if targets.ConsumerGroupReference != nil { + p.ConsumerGroup = &kong.ConsumerGroup{Name: kong.String(targets.ConsumerGroupReference.Name)} + } + + return []kong.Plugin{p}, nil +} + +// --------------------------------------------------------------------------- +// Utility helpers +// --------------------------------------------------------------------------- + +// convertViaJSON marshals src to JSON then unmarshals into dst. +// This acts as a bridge between sdkkonnectcomp.* and kong.* types +// that share the same JSON field names (both generated from Kong API spec). +func convertViaJSON(src, dst any) error { + b, err := json.Marshal(src) + if err != nil { + return err + } + return json.Unmarshal(b, dst) +} + +// tagsFromV1Alpha1 merges CRD-level tags with generated k8s metadata tags. +func tagsFromV1Alpha1(specTags []string, k8sTags []*string) []*string { + result := make([]*string, 0, len(specTags)+len(k8sTags)) + for i := range specTags { + result = append(result, &specTags[i]) + } + result = append(result, k8sTags...) + return result +} + +// stringSliceToKongStringSlice converts []string → []*string. +func stringSliceToKongStringSlice(in []string) []*string { + out := make([]*string, len(in)) + for i := range in { + s := in[i] + out[i] = &s + } + return out +} + diff --git a/ingress-controller/internal/dataplane/translator/translator.go b/ingress-controller/internal/dataplane/translator/translator.go index d4ead6489b..af8616b3b9 100644 --- a/ingress-controller/internal/dataplane/translator/translator.go +++ b/ingress-controller/internal/dataplane/translator/translator.go @@ -54,6 +54,12 @@ type FeatureFlags struct { // KongCustomEntity indicates whether we should support translating custom entities from KongCustomEntity CRs. KongCustomEntity bool + // KongServiceV1Alpha1 enables KIC standalone mode for v1alpha1 Kong Gateway entity CRDs + // (KongService, KongRoute, KongUpstream, KongTarget, KongCertificate, KongCACertificate, + // KongSNI, KongPluginBinding). When enabled, these CRDs are translated directly to KongState + // (kong.* types) and pushed to Kong via POST /config in db-less mode without Konnect. + KongServiceV1Alpha1 bool + // CombinedServicesFromDifferentHTTPRoutes indicates whether we should combine rules from different HTTPRoutes // that are sharing the same combination of backends to one Kong service. CombinedServicesFromDifferentHTTPRoutes bool @@ -81,6 +87,7 @@ func NewFeatureFlags( RewriteURIs: featureGates.Enabled(managercfg.RewriteURIsFeature), KongServiceFacade: featureGates.Enabled(managercfg.KongServiceFacadeFeature), KongCustomEntity: featureGates.Enabled(managercfg.KongCustomEntityFeature), + KongServiceV1Alpha1: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), CombinedServicesFromDifferentHTTPRoutes: combinedServicesFromDifferentHTTPRoutes, SupportRedirectPlugin: supportRedirectPlugin, } @@ -229,6 +236,13 @@ func (t *Translator) BuildKongConfig() KongConfigBuildingResult { t.registerSuccessfullyTranslatedObject(result.Vaults[i].K8sKongVault) } + // process v1alpha1 Kong Gateway entity CRDs in KIC standalone mode (no Konnect required) + if t.featureFlags.KongServiceV1Alpha1 { + result.FillFromKongServicesV1Alpha1(t.logger, t.storer, t.failuresCollector) + result.FillFromKongCertificatesV1Alpha1(t.logger, t.storer, t.failuresCollector) + result.FillFromKongPluginBindingsV1Alpha1(t.logger, t.storer, t.failuresCollector) + } + // process consumer groups result.FillConsumerGroups(t.logger, t.storer) for i := range result.ConsumerGroups { diff --git a/ingress-controller/internal/manager/controllerdef.go b/ingress-controller/internal/manager/controllerdef.go index fa8abaa7ea..ac6d926d23 100644 --- a/ingress-controller/internal/manager/controllerdef.go +++ b/ingress-controller/internal/manager/controllerdef.go @@ -285,6 +285,111 @@ func setupControllers( StatusQueue: kubernetesStatusQueue, }, }, + // v1alpha1 Kong Gateway entity controllers (KIC standalone mode). + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongServiceReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongServiceV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongRouteReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongRouteV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongUpstreamReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongUpstreamV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongTargetReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongTargetV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongCertificateReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongCertificateV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongCACertificateReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongCACertificateV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongSNIReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongSNIV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, + { + Enabled: featureGates.Enabled(managercfg.KongServiceV1Alpha1Feature), + Controller: &configuration.KongV1Alpha1KongPluginBindingReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongPluginBindingV1Alpha1"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, // --------------------------------------------------------------------------- // Gateway API Controllers // --------------------------------------------------------------------------- diff --git a/ingress-controller/internal/store/fake_store.go b/ingress-controller/internal/store/fake_store.go index 16f65d292c..2528dde7a8 100644 --- a/ingress-controller/internal/store/fake_store.go +++ b/ingress-controller/internal/store/fake_store.go @@ -56,6 +56,15 @@ type FakeObjects struct { KongServiceFacades []*incubatorv1alpha1.KongServiceFacade KongVaults []*configurationv1alpha1.KongVault KongCustomEntities []*configurationv1alpha1.KongCustomEntity + + KongServicesV1Alpha1 []*configurationv1alpha1.KongService + KongRoutesV1Alpha1 []*configurationv1alpha1.KongRoute + KongUpstreamsV1Alpha1 []*configurationv1alpha1.KongUpstream + KongTargetsV1Alpha1 []*configurationv1alpha1.KongTarget + KongCertificatesV1Alpha1 []*configurationv1alpha1.KongCertificate + KongCACertificatesV1Alpha1 []*configurationv1alpha1.KongCACertificate + KongSNIsV1Alpha1 []*configurationv1alpha1.KongSNI + KongPluginBindingsV1Alpha1 []*configurationv1alpha1.KongPluginBinding } // NewFakeStore creates a store backed by the objects passed in as arguments. @@ -217,6 +226,54 @@ func NewFakeStore( return nil, err } } + kongServiceV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongServicesV1Alpha1 { + if err := kongServiceV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongRouteV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongRoutesV1Alpha1 { + if err := kongRouteV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongUpstreamV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongUpstreamsV1Alpha1 { + if err := kongUpstreamV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongTargetV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongTargetsV1Alpha1 { + if err := kongTargetV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongCertificateV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongCertificatesV1Alpha1 { + if err := kongCertificateV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongCACertificateV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongCACertificatesV1Alpha1 { + if err := kongCACertificateV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongSNIV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongSNIsV1Alpha1 { + if err := kongSNIV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } + kongPluginBindingV1Alpha1Store := cache.NewStore(namespacedKeyFunc) + for _, e := range objects.KongPluginBindingsV1Alpha1 { + if err := kongPluginBindingV1Alpha1Store.Add(e); err != nil { + return nil, err + } + } s = &Store{ stores: CacheStores{ @@ -245,6 +302,14 @@ func NewFakeStore( KongServiceFacade: kongServiceFacade, KongVault: kongVaultStore, KongCustomEntity: kongCustomEntityStore, + KongServiceV1Alpha1: kongServiceV1Alpha1Store, + KongRouteV1Alpha1: kongRouteV1Alpha1Store, + KongUpstreamV1Alpha1: kongUpstreamV1Alpha1Store, + KongTargetV1Alpha1: kongTargetV1Alpha1Store, + KongCertificateV1Alpha1: kongCertificateV1Alpha1Store, + KongCACertificateV1Alpha1: kongCACertificateV1Alpha1Store, + KongSNIV1Alpha1: kongSNIV1Alpha1Store, + KongPluginBindingV1Alpha1: kongPluginBindingV1Alpha1Store, }, ingressClass: annotations.DefaultIngressClass, isValidIngressClass: annotations.IngressClassValidatorFuncFromObjectMeta(annotations.DefaultIngressClass), diff --git a/ingress-controller/internal/store/store.go b/ingress-controller/internal/store/store.go index 3543046686..378a092d8f 100644 --- a/ingress-controller/internal/store/store.go +++ b/ingress-controller/internal/store/store.go @@ -87,6 +87,16 @@ type Storer interface { ListKongVaults() []*configurationv1alpha1.KongVault ListKongCustomEntities() []*configurationv1alpha1.KongCustomEntity + // v1alpha1 Kong Gateway entity types (KIC standalone). + ListKongServicesV1Alpha1() []*configurationv1alpha1.KongService + ListKongRoutesV1Alpha1() []*configurationv1alpha1.KongRoute + ListKongUpstreamsV1Alpha1() []*configurationv1alpha1.KongUpstream + ListKongTargetsV1Alpha1() []*configurationv1alpha1.KongTarget + ListKongCertificatesV1Alpha1() []*configurationv1alpha1.KongCertificate + ListKongCACertificatesV1Alpha1() []*configurationv1alpha1.KongCACertificate + ListKongSNIsV1Alpha1() []*configurationv1alpha1.KongSNI + ListKongPluginBindingsV1Alpha1() []*configurationv1alpha1.KongPluginBinding + // Ingress API resources. GetIngressClassName() string GetIngressClassV1(name string) (*netv1.IngressClass, error) @@ -706,6 +716,94 @@ func (s Store) ListKongCustomEntities() []*configurationv1alpha1.KongCustomEntit return kongCustomEntities } +func (s Store) ListKongServicesV1Alpha1() []*configurationv1alpha1.KongService { + var items []*configurationv1alpha1.KongService + for _, obj := range s.stores.KongServiceV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongService) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongRoutesV1Alpha1() []*configurationv1alpha1.KongRoute { + var items []*configurationv1alpha1.KongRoute + for _, obj := range s.stores.KongRouteV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongRoute) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongUpstreamsV1Alpha1() []*configurationv1alpha1.KongUpstream { + var items []*configurationv1alpha1.KongUpstream + for _, obj := range s.stores.KongUpstreamV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongUpstream) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongTargetsV1Alpha1() []*configurationv1alpha1.KongTarget { + var items []*configurationv1alpha1.KongTarget + for _, obj := range s.stores.KongTargetV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongTarget) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongCertificatesV1Alpha1() []*configurationv1alpha1.KongCertificate { + var items []*configurationv1alpha1.KongCertificate + for _, obj := range s.stores.KongCertificateV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongCertificate) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongCACertificatesV1Alpha1() []*configurationv1alpha1.KongCACertificate { + var items []*configurationv1alpha1.KongCACertificate + for _, obj := range s.stores.KongCACertificateV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongCACertificate) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongSNIsV1Alpha1() []*configurationv1alpha1.KongSNI { + var items []*configurationv1alpha1.KongSNI + for _, obj := range s.stores.KongSNIV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongSNI) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + +func (s Store) ListKongPluginBindingsV1Alpha1() []*configurationv1alpha1.KongPluginBinding { + var items []*configurationv1alpha1.KongPluginBinding + for _, obj := range s.stores.KongPluginBindingV1Alpha1.List() { + item, ok := obj.(*configurationv1alpha1.KongPluginBinding) + if ok && s.isValidIngressClass(&item.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + items = append(items, item) + } + } + return items +} + // getIngressClassHandling returns annotations.ExactOrEmptyClassMatch if an IngressClass is the default class, or // annotations.ExactClassMatch if the IngressClass is not default or does not exist. func (s Store) getIngressClassHandling() annotations.ClassMatching { diff --git a/ingress-controller/internal/store/zz_generated.cache_stores.go b/ingress-controller/internal/store/zz_generated.cache_stores.go index ecf823ccef..2ab6f5ce63 100644 --- a/ingress-controller/internal/store/zz_generated.cache_stores.go +++ b/ingress-controller/internal/store/zz_generated.cache_stores.go @@ -46,6 +46,14 @@ type CacheStores struct { KongServiceFacade cache.Store KongVault cache.Store KongCustomEntity cache.Store + KongServiceV1Alpha1 cache.Store + KongRouteV1Alpha1 cache.Store + KongUpstreamV1Alpha1 cache.Store + KongTargetV1Alpha1 cache.Store + KongCertificateV1Alpha1 cache.Store + KongCACertificateV1Alpha1 cache.Store + KongSNIV1Alpha1 cache.Store + KongPluginBindingV1Alpha1 cache.Store l *sync.RWMutex } @@ -76,6 +84,14 @@ func NewCacheStores() CacheStores { KongServiceFacade: cache.NewStore(namespacedKeyFunc), KongVault: cache.NewStore(clusterWideKeyFunc), KongCustomEntity: cache.NewStore(namespacedKeyFunc), + KongServiceV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongRouteV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongUpstreamV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongTargetV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongCertificateV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongCACertificateV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongSNIV1Alpha1: cache.NewStore(namespacedKeyFunc), + KongPluginBindingV1Alpha1: cache.NewStore(namespacedKeyFunc), l: &sync.RWMutex{}, } @@ -133,6 +149,22 @@ func (c CacheStores) Get(obj runtime.Object) (item interface{}, exists bool, err return c.KongVault.Get(obj) case *kongv1alpha1.KongCustomEntity: return c.KongCustomEntity.Get(obj) + case *kongv1alpha1.KongService: + return c.KongServiceV1Alpha1.Get(obj) + case *kongv1alpha1.KongRoute: + return c.KongRouteV1Alpha1.Get(obj) + case *kongv1alpha1.KongUpstream: + return c.KongUpstreamV1Alpha1.Get(obj) + case *kongv1alpha1.KongTarget: + return c.KongTargetV1Alpha1.Get(obj) + case *kongv1alpha1.KongCertificate: + return c.KongCertificateV1Alpha1.Get(obj) + case *kongv1alpha1.KongCACertificate: + return c.KongCACertificateV1Alpha1.Get(obj) + case *kongv1alpha1.KongSNI: + return c.KongSNIV1Alpha1.Get(obj) + case *kongv1alpha1.KongPluginBinding: + return c.KongPluginBindingV1Alpha1.Get(obj) } return nil, false, fmt.Errorf("%T is not a supported cache object type", obj) } @@ -190,6 +222,22 @@ func (c CacheStores) Add(obj runtime.Object) error { return c.KongVault.Add(obj) case *kongv1alpha1.KongCustomEntity: return c.KongCustomEntity.Add(obj) + case *kongv1alpha1.KongService: + return c.KongServiceV1Alpha1.Add(obj) + case *kongv1alpha1.KongRoute: + return c.KongRouteV1Alpha1.Add(obj) + case *kongv1alpha1.KongUpstream: + return c.KongUpstreamV1Alpha1.Add(obj) + case *kongv1alpha1.KongTarget: + return c.KongTargetV1Alpha1.Add(obj) + case *kongv1alpha1.KongCertificate: + return c.KongCertificateV1Alpha1.Add(obj) + case *kongv1alpha1.KongCACertificate: + return c.KongCACertificateV1Alpha1.Add(obj) + case *kongv1alpha1.KongSNI: + return c.KongSNIV1Alpha1.Add(obj) + case *kongv1alpha1.KongPluginBinding: + return c.KongPluginBindingV1Alpha1.Add(obj) } return fmt.Errorf("cannot add unsupported kind %q to the store", obj.GetObjectKind().GroupVersionKind()) } @@ -247,6 +295,22 @@ func (c CacheStores) Delete(obj runtime.Object) error { return c.KongVault.Delete(obj) case *kongv1alpha1.KongCustomEntity: return c.KongCustomEntity.Delete(obj) + case *kongv1alpha1.KongService: + return c.KongServiceV1Alpha1.Delete(obj) + case *kongv1alpha1.KongRoute: + return c.KongRouteV1Alpha1.Delete(obj) + case *kongv1alpha1.KongUpstream: + return c.KongUpstreamV1Alpha1.Delete(obj) + case *kongv1alpha1.KongTarget: + return c.KongTargetV1Alpha1.Delete(obj) + case *kongv1alpha1.KongCertificate: + return c.KongCertificateV1Alpha1.Delete(obj) + case *kongv1alpha1.KongCACertificate: + return c.KongCACertificateV1Alpha1.Delete(obj) + case *kongv1alpha1.KongSNI: + return c.KongSNIV1Alpha1.Delete(obj) + case *kongv1alpha1.KongPluginBinding: + return c.KongPluginBindingV1Alpha1.Delete(obj) } return fmt.Errorf("cannot delete unsupported kind %q from the store", obj.GetObjectKind().GroupVersionKind()) } @@ -277,6 +341,14 @@ func (c CacheStores) ListAllStores() []cache.Store { c.KongServiceFacade, c.KongVault, c.KongCustomEntity, + c.KongServiceV1Alpha1, + c.KongRouteV1Alpha1, + c.KongUpstreamV1Alpha1, + c.KongTargetV1Alpha1, + c.KongCertificateV1Alpha1, + c.KongCACertificateV1Alpha1, + c.KongSNIV1Alpha1, + c.KongPluginBindingV1Alpha1, } } @@ -306,5 +378,13 @@ func (c CacheStores) SupportedTypes() []client.Object { &incubatorv1alpha1.KongServiceFacade{}, &kongv1alpha1.KongVault{}, &kongv1alpha1.KongCustomEntity{}, + &kongv1alpha1.KongService{}, + &kongv1alpha1.KongRoute{}, + &kongv1alpha1.KongUpstream{}, + &kongv1alpha1.KongTarget{}, + &kongv1alpha1.KongCertificate{}, + &kongv1alpha1.KongCACertificate{}, + &kongv1alpha1.KongSNI{}, + &kongv1alpha1.KongPluginBinding{}, } } diff --git a/ingress-controller/pkg/manager/config/feature_gates_keys.go b/ingress-controller/pkg/manager/config/feature_gates_keys.go index a43c062004..15fbe6cb4d 100644 --- a/ingress-controller/pkg/manager/config/feature_gates_keys.go +++ b/ingress-controller/pkg/manager/config/feature_gates_keys.go @@ -26,6 +26,13 @@ const ( // for configuring custom Kong entities that KIC does not support yet. // Requires feature gate `FillIDs` to be enabled. KongCustomEntityFeature = "KongCustomEntity" + + // KongServiceV1Alpha1Feature is the name of the feature-gate for enabling KIC standalone support + // for the v1alpha1 Kong Gateway entity CRDs (KongService, KongRoute, KongUpstream, KongTarget, + // KongCertificate, KongCACertificate, KongSNI, KongPluginBinding). + // When enabled, these CRDs are translated directly to KongState (kong.*) and pushed via POST /config + // in db-less mode, without requiring a Konnect control plane. + KongServiceV1Alpha1Feature = "KongServiceV1Alpha1" ) // GetFeatureGatesDefaults returns the default values for all feature gates. @@ -40,5 +47,6 @@ func GetFeatureGatesDefaults() FeatureGates { SanitizeKonnectConfigDumpsFeature: true, FallbackConfigurationFeature: false, KongCustomEntityFeature: true, + KongServiceV1Alpha1Feature: false, } }