From 01a209305076a1d1372d6a842db89858d38e9e4a Mon Sep 17 00:00:00 2001 From: Tao Yi Date: Tue, 9 Jan 2024 02:24:44 +0800 Subject: [PATCH] feat(vault) implement controller and translator (#5384) Add controller and translator for KongVault resources. --- CHANGELOG.md | 10 +- .../validating_webhook_configuration.yaml | 2 + config/crd/kustomization.yaml | 1 + config/rbac/role.yaml | 16 ++ docs/cli-arguments.md | 1 + hack/deploy-admission-controller.sh | 1 + .../generators/controllers/networking/main.go | 17 ++ internal/admission/adminapi_provider.go | 8 + internal/admission/errors.go | 3 + internal/admission/handler.go | 22 ++ internal/admission/server_test.go | 5 + internal/admission/validator.go | 42 ++++ internal/admission/validator_test.go | 96 +++++++++ .../configuration/zz_generated_controllers.go | 170 +++++++++++++++ internal/dataplane/deckgen/generate.go | 12 ++ internal/dataplane/failures/failures.go | 2 + internal/dataplane/kongstate/kongstate.go | 38 ++++ .../dataplane/kongstate/kongstate_test.go | 167 +++++++++++++++ internal/dataplane/kongstate/plugin.go | 18 +- internal/dataplane/kongstate/util.go | 16 ++ internal/dataplane/kongstate/vault.go | 13 ++ internal/dataplane/translator/translator.go | 6 + internal/manager/config.go | 2 + internal/manager/controllerdef.go | 13 ++ internal/store/cache_stores.go | 8 + internal/store/fake_store.go | 11 + internal/store/store.go | 25 +++ .../all-in-one-dbless-k4k8s-enterprise.yaml | 194 ++++++++++++++++++ .../all-in-one-dbless-konnect-enterprise.yaml | 194 ++++++++++++++++++ .../manifests/all-in-one-dbless-konnect.yaml | 194 ++++++++++++++++++ test/e2e/manifests/all-in-one-dbless.yaml | 194 ++++++++++++++++++ .../all-in-one-postgres-enterprise.yaml | 194 ++++++++++++++++++ ...all-in-one-postgres-multiple-gateways.yaml | 194 ++++++++++++++++++ test/e2e/manifests/all-in-one-postgres.yaml | 194 ++++++++++++++++++ test/integration/suite_test.go | 2 + test/integration/vault_test.go | 141 +++++++++++++ 36 files changed, 2208 insertions(+), 18 deletions(-) create mode 100644 internal/dataplane/kongstate/vault.go create mode 100644 test/integration/vault_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 2860210a51..570dd36e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,9 +129,13 @@ Adding a new version? You'll need three changes: - Added functionality to the `KongUpstreamPolicy` controller to properly set and enforce `KongUpstreamPolicy` status. [#5185](https://github.com/Kong/kubernetes-ingress-controller/pull/5185) -- Add CRD `KongVault` to reperesent a custom Kong vault for storing senstive - data used in plugin configurations. +- New CRD `KongVault` to reperesent a custom Kong vault for storing senstive + data used in plugin configurations. Now users can create a `KongVault` to + create a custom Kong vault and reference the values in configurations of + plugins. Reference of using Kong vaults: [Kong vault] [#5354](https://github.com/Kong/kubernetes-ingress-controller/pull/5354) + [#5384](https://github.com/Kong/kubernetes-ingress-controller/pull/5384) + ### Fixed @@ -165,6 +169,8 @@ Adding a new version? You'll need three changes: - `KongPlugin` and `KongClusterPlugin` now enforce `plugin` to be immutable. [#5142](https://github.com/Kong/kubernetes-ingress-controller/pull/5142) +[Kong vault]: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/ + ## [3.0.1] > Release date: 2023-11-22 diff --git a/config/components/manager_dev_webhook/validating_webhook_configuration.yaml b/config/components/manager_dev_webhook/validating_webhook_configuration.yaml index 5124fce767..d7ffe466f5 100644 --- a/config/components/manager_dev_webhook/validating_webhook_configuration.yaml +++ b/config/components/manager_dev_webhook/validating_webhook_configuration.yaml @@ -23,9 +23,11 @@ webhooks: - UPDATE resources: - kongconsumers + - kongconsumergroups - kongplugins - kongclusterplugins - kongingresses + - kongvaults scope: '*' - apiGroups: - "" diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 1dafe90634..12ac6ec308 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -13,6 +13,7 @@ resources: - bases/configuration.konghq.com_kongplugins.yaml - bases/configuration.konghq.com_ingressclassparameterses.yaml - bases/configuration.konghq.com_kongupstreampolicies.yaml +- bases/configuration.konghq.com_kongvaults.yaml #+kubebuilder:scaffold:crdkustomizeresource # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dc6f6f02b6..43e48dd44a 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -153,6 +153,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/docs/cli-arguments.md b/docs/cli-arguments.md index 79036e7566..86a0abec43 100644 --- a/docs/cli-arguments.md +++ b/docs/cli-arguments.md @@ -27,6 +27,7 @@ | `--enable-controller-ingress-networkingv1` | `bool` | Enable the networking.k8s.io/v1 Ingress controller. | `true` | | `--enable-controller-kong-service-facade` | `bool` | Enable the KongServiceFacade controller. | `true` | | `--enable-controller-kong-upstream-policy` | `bool` | Enable the KongUpstreamPolicy controller. | `true` | +| `--enable-controller-kong-vault` | `bool` | Enable the KongVault controller. | `true` | | `--enable-controller-kongclusterplugin` | `bool` | Enable the KongClusterPlugin controller. | `true` | | `--enable-controller-kongconsumer` | `bool` | Enable the KongConsumer controller. | `true` | | `--enable-controller-kongingress` | `bool` | Enable the KongIngress controller. | `true` | diff --git a/hack/deploy-admission-controller.sh b/hack/deploy-admission-controller.sh index 5d7cf3aab6..9731c9e318 100755 --- a/hack/deploy-admission-controller.sh +++ b/hack/deploy-admission-controller.sh @@ -59,6 +59,7 @@ webhooks: - kongplugins - kongclusterplugins - kongingresses + - kongvaults - apiGroups: - '' apiVersions: diff --git a/hack/generators/controllers/networking/main.go b/hack/generators/controllers/networking/main.go index a0b1cc75ba..3adcc5ef1d 100644 --- a/hack/generators/controllers/networking/main.go +++ b/hack/generators/controllers/networking/main.go @@ -247,6 +247,23 @@ var inputControllersNeeded = &typesNeeded{ AcceptsIngressClassNameAnnotation: true, RBACVerbs: []string{"get", "list", "watch"}, }, + typeNeeded{ + Group: "configuration.konghq.com", + Version: "v1alpha1", + Kind: "KongVault", + PackageImportAlias: "kongv1alpha1", + PackageAlias: "KongV1Alpha1", + Package: kongv1alpha1, + Plural: "kongvaults", + CacheType: "KongVault", + NeedsStatusPermissions: true, + ConfigStatusNotificationsEnabled: true, + ProgrammedCondition: ProgrammedConditionConfiguration{ + UpdatesEnabled: true, + }, + AcceptsIngressClassNameAnnotation: true, + RBACVerbs: []string{"get", "list", "watch"}, + }, } var inputRBACPermissionsNeeded = &rbacsNeeded{ diff --git a/internal/admission/adminapi_provider.go b/internal/admission/adminapi_provider.go index 051da681b8..7868c3d67c 100644 --- a/internal/admission/adminapi_provider.go +++ b/internal/admission/adminapi_provider.go @@ -61,6 +61,14 @@ func (p DefaultAdminAPIServicesProvider) GetRoutesService() (kong.AbstractRouteS return c.Routes, true } +func (p DefaultAdminAPIServicesProvider) GetVaultsService() (kong.AbstractVaultService, bool) { + c, ok := p.designatedAdminAPIClient() + if !ok { + return nil, ok + } + return c.Vaults, true +} + func (p DefaultAdminAPIServicesProvider) designatedAdminAPIClient() (*kong.Client, bool) { gwClients := p.gatewayClientsProvider.GatewayClients() if len(gwClients) == 0 { diff --git a/internal/admission/errors.go b/internal/admission/errors.go index bd20444e8c..33917f8bec 100644 --- a/internal/admission/errors.go +++ b/internal/admission/errors.go @@ -14,6 +14,9 @@ const ( ErrTextPluginConfigValidationFailed = "unable to validate plugin schema" ErrTextPluginConfigViolatesSchema = "plugin failed schema validation: %s" ErrTextPluginSecretConfigUnretrievable = "could not load secret plugin configuration" + ErrTextVaultConfigUnmarshalFailed = "failed to unmarshal vault configuration: %v" + ErrTextVaultUnableToValidate = "unable to validate vault on Kong gateway" + ErrTextVaultConfigValidationResultInvalid = "vault configuration in invalid: %s" ) const ( diff --git a/internal/admission/handler.go b/internal/admission/handler.go index bb1b6b35ae..01abf25d47 100644 --- a/internal/admission/handler.go +++ b/internal/admission/handler.go @@ -17,6 +17,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi" "github.com/kong/kubernetes-ingress-controller/v3/internal/util" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" ) @@ -96,6 +97,11 @@ var ( Version: kongv1.SchemeGroupVersion.Version, Resource: "kongingresses", } + kongVaultGVResource = metav1.GroupVersionResource{ + Group: kongv1alpha1.SchemeGroupVersion.Group, + Version: kongv1alpha1.SchemeGroupVersion.Version, + Resource: "kongvaults", + } secretGVResource = metav1.GroupVersionResource{ Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, @@ -135,6 +141,8 @@ func (h RequestHandler) handleValidation(ctx context.Context, request admissionv return h.handleHTTPRoute(ctx, request, responseBuilder) case kongIngressGVResource: return h.handleKongIngress(ctx, request, responseBuilder) + case kongVaultGVResource: + return h.handleKongVault(ctx, request, responseBuilder) case serviceGVResource: return h.handleService(ctx, request, responseBuilder) case ingressGVResource: @@ -426,3 +434,17 @@ func (h RequestHandler) handleIngress(ctx context.Context, request admissionv1.A return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil } + +func (h RequestHandler) handleKongVault(ctx context.Context, request admissionv1.AdmissionRequest, responseBuilder *ResponseBuilder) (*admissionv1.AdmissionResponse, error) { + kongVault := kongv1alpha1.KongVault{} + _, _, err := codecs.UniversalDeserializer().Decode(request.Object.Raw, nil, &kongVault) + if err != nil { + return nil, err + } + ok, message, err := h.Validator.ValidateVault(ctx, kongVault) + if err != nil { + return nil, err + } + + return responseBuilder.Allowed(ok).WithMessage(message).Build(), nil +} diff --git a/internal/admission/server_test.go b/internal/admission/server_test.go index f927ffce8f..e835931205 100644 --- a/internal/admission/server_test.go +++ b/internal/admission/server_test.go @@ -20,6 +20,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/gatewayapi" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" ) @@ -77,6 +78,10 @@ func (v KongFakeValidator) ValidateIngress(_ context.Context, _ netv1.Ingress) ( return v.Result, v.Message, v.Error } +func (v KongFakeValidator) ValidateVault(_ context.Context, _ kongv1alpha1.KongVault) (bool, string, error) { + return v.Result, v.Message, v.Error +} + func TestServeHTTPBasic(t *testing.T) { assert := assert.New(t) res := httptest.NewRecorder() diff --git a/internal/admission/validator.go b/internal/admission/validator.go index 4513ee6b19..80c62443d5 100644 --- a/internal/admission/validator.go +++ b/internal/admission/validator.go @@ -26,6 +26,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/store" "github.com/kong/kubernetes-ingress-controller/v3/internal/util" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" ) @@ -35,6 +36,7 @@ type KongValidator interface { ValidateConsumerGroup(ctx context.Context, consumerGroup kongv1beta1.KongConsumerGroup) (bool, string, error) ValidatePlugin(ctx context.Context, plugin kongv1.KongPlugin, overrideSecrets []*corev1.Secret) (bool, string, error) ValidateClusterPlugin(ctx context.Context, plugin kongv1.KongClusterPlugin, overrideSecrets []*corev1.Secret) (bool, string, error) + ValidateVault(ctx context.Context, vault kongv1alpha1.KongVault) (bool, string, error) ValidateCredential(ctx context.Context, secret corev1.Secret) (bool, string) ValidateGateway(ctx context.Context, gateway gatewayapi.Gateway) (bool, string, error) ValidateHTTPRoute(ctx context.Context, httproute gatewayapi.HTTPRoute) (bool, string, error) @@ -49,6 +51,7 @@ type AdminAPIServicesProvider interface { GetConsumerGroupsService() (kong.AbstractConsumerGroupService, bool) GetInfoService() (kong.AbstractInfoService, bool) GetRoutesService() (kong.AbstractRouteService, bool) + GetVaultsService() (kong.AbstractVaultService, bool) } // ConsumerGetter is an interface for retrieving KongConsumers. @@ -520,6 +523,30 @@ func (noOpRoutesValidator) Validate(_ context.Context, _ *kong.Route) (bool, str return true, "", nil } +func (validator KongHTTPValidator) ValidateVault(ctx context.Context, k8sKongVault kongv1alpha1.KongVault) (bool, string, error) { + // Ignore KongVaults that are being managed by another controller. + if !validator.ingressClassMatcher(&k8sKongVault.ObjectMeta, annotations.IngressClassKey, annotations.ExactClassMatch) { + return true, "", nil + } + config, err := kongstate.RawConfigToConfiguration(k8sKongVault.Spec.Config.Raw) + if err != nil { + return false, fmt.Sprintf(ErrTextVaultConfigUnmarshalFailed, err), nil + } + kongVault := kong.Vault{ + Name: kong.String(k8sKongVault.Spec.Backend), + Prefix: kong.String(k8sKongVault.Spec.Prefix), + Description: kong.String(k8sKongVault.Spec.Description), + Config: config, + } + // TODO: /schemas/vaults/test does not check "unique" restraint on `prefix` field: + // https://github.com/Kong/kubernetes-ingress-controller/issues/5395 + errText, err := validator.validateVaultAgainstGatewaySchema(ctx, kongVault) + if err != nil || errText != "" { + return false, errText, err + } + return true, "", nil +} + // ----------------------------------------------------------------------------- // KongHTTPValidator - Private Methods // ----------------------------------------------------------------------------- @@ -582,6 +609,21 @@ func (validator KongHTTPValidator) validatePluginAgainstGatewaySchema(ctx contex return "", nil } +func (validator KongHTTPValidator) validateVaultAgainstGatewaySchema(ctx context.Context, vault kong.Vault) (string, error) { + vaultService, hasClient := validator.AdminAPIServicesProvider.GetVaultsService() + if !hasClient { + return "", nil + } + isValid, msg, err := vaultService.Validate(ctx, &vault) + if err != nil { + return ErrTextVaultUnableToValidate, err + } + if !isValid { + return fmt.Sprintf(ErrTextVaultConfigValidationResultInvalid, msg), nil + } + return "", nil +} + type managerClientSecretGetter struct { managerClient client.Client } diff --git a/internal/admission/validator_test.go b/internal/admission/validator_test.go index 1bc011bdc1..4bb6b4bda8 100644 --- a/internal/admission/validator_test.go +++ b/internal/admission/validator_test.go @@ -28,6 +28,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/store" "github.com/kong/kubernetes-ingress-controller/v3/internal/util/builder" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" incubatorv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/incubator/v1alpha1" ) @@ -62,6 +63,7 @@ type fakeServicesProvider struct { consumerGroupSvc kong.AbstractConsumerGroupService infoSvc kong.AbstractInfoService routeSvc kong.AbstractRouteService + vaultSvc kong.AbstractVaultService } func (f fakeServicesProvider) GetConsumersService() (kong.AbstractConsumerService, bool) { @@ -99,6 +101,13 @@ func (f fakeServicesProvider) GetRoutesService() (kong.AbstractRouteService, boo return nil, false } +func (f fakeServicesProvider) GetVaultsService() (kong.AbstractVaultService, bool) { + if f.vaultSvc != nil { + return f.vaultSvc, true + } + return nil, false +} + func TestKongHTTPValidator_ValidatePlugin(t *testing.T) { store, _ := store.NewFakeStore(store.FakeObjects{ Secrets: []*corev1.Secret{ @@ -1227,3 +1236,90 @@ func (f *fakeRouteSvc) Validate(context.Context, *kong.Route) (bool, string, err } return true, "", nil } + +func TestValidator_ValidateVault(t *testing.T) { + testCases := []struct { + name string + kongVault kongv1alpha1.KongVault + validateSvcFail bool + expectedOK bool + expectedMessage string + expectedError string + }{ + { + name: "valid vault", + kongVault: kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-1", + }, + }, + expectedOK: true, + }, + { + name: "vault with invalid(malformed) configuration", + kongVault: kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-1", + Config: apiextensionsv1.JSON{ + Raw: []byte(`{{}`), + }, + }, + }, + expectedOK: false, + expectedMessage: "failed to unmarshal vault configuration", + }, + { + name: "vault with failure in validating service", + kongVault: kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-1", + }, + }, + validateSvcFail: true, + expectedOK: false, + expectedMessage: "something is wrong with the vault", + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + validator := KongHTTPValidator{ + AdminAPIServicesProvider: fakeServicesProvider{ + vaultSvc: &fakeVaultSvc{ + shouldFail: tc.validateSvcFail, + }, + }, + ingressClassMatcher: fakeClassMatcher, + Logger: logr.Discard(), + } + ok, msg, err := validator.ValidateVault(context.Background(), tc.kongVault) + require.NoError(t, err) + assert.Equal(t, tc.expectedOK, ok) + assert.Contains(t, msg, tc.expectedMessage) + }) + } +} + +type fakeVaultSvc struct { + kong.AbstractVaultService + shouldFail bool +} + +func (s fakeVaultSvc) Validate(context.Context, *kong.Vault) (bool, string, error) { + if s.shouldFail { + return false, "something is wrong with the vault", nil + } + return true, "", nil +} diff --git a/internal/controllers/configuration/zz_generated_controllers.go b/internal/controllers/configuration/zz_generated_controllers.go index ed4c3a8986..fadccec529 100644 --- a/internal/controllers/configuration/zz_generated_controllers.go +++ b/internal/controllers/configuration/zz_generated_controllers.go @@ -1903,6 +1903,176 @@ func (r *IncubatorV1Alpha1KongServiceFacadeReconciler) Reconcile(ctx context.Con return ctrl.Result{}, nil } +// ----------------------------------------------------------------------------- +// KongV1Alpha1 KongVault - Reconciler +// ----------------------------------------------------------------------------- + +// KongV1Alpha1KongVaultReconciler reconciles KongVault resources +type KongV1Alpha1KongVaultReconciler 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 = &KongV1Alpha1KongVaultReconciler{} + +// SetupWithManager sets up the controller with the Manager. +func (r *KongV1Alpha1KongVaultReconciler) SetupWithManager(mgr ctrl.Manager) error { + c, err := controller.New("KongV1Alpha1KongVault", mgr, controller.Options{ + Reconciler: r, + LogConstructor: func(_ *reconcile.Request) logr.Logger { + return r.Log + }, + CacheSyncTimeout: r.CacheSyncTimeout, + }) + if err != nil { + return err + } + // if configured, start the status updater controller + if r.StatusQueue != nil { + if err := c.Watch( + &source.Channel{Source: r.StatusQueue.Subscribe(schema.GroupVersionKind{ + Group: "configuration.konghq.com", + Version: "v1alpha1", + Kind: "KongVault", + })}, + &handler.EnqueueRequestForObject{}, + ); err != nil { + return err + } + } + if !r.DisableIngressClassLookups { + err = c.Watch( + source.Kind(mgr.GetCache(), &netv1.IngressClass{}), + handler.EnqueueRequestsFromMapFunc(r.listClassless), + predicate.NewPredicateFuncs(ctrlutils.IsDefaultIngressClass), + ) + if err != nil { + return err + } + } + preds := ctrlutils.GeneratePredicateFuncsForIngressClassFilter(r.IngressClassName) + return c.Watch( + source.Kind(mgr.GetCache(), &kongv1alpha1.KongVault{}), + &handler.EnqueueRequestForObject{}, + preds, + ) +} + +// listClassless finds and reconciles all objects without ingress class information +func (r *KongV1Alpha1KongVaultReconciler) listClassless(ctx context.Context, obj client.Object) []reconcile.Request { + resourceList := &kongv1alpha1.KongVaultList{} + if err := r.Client.List(ctx, resourceList); err != nil { + r.Log.Error(err, "Failed to list classless kongvaults") + 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 +} + +// SetLogger sets the logger. +func (r *KongV1Alpha1KongVaultReconciler) SetLogger(l logr.Logger) { + r.Log = l +} + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults/status,verbs=get;update;patch + +// Reconcile processes the watched objects +func (r *KongV1Alpha1KongVaultReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := r.Log.WithValues("KongV1Alpha1KongVault", req.NamespacedName) + + // get the relevant object + obj := new(kongv1alpha1.KongVault) + + 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(util.DebugLevel).Info("Reconciling resource", "namespace", req.Namespace, "name", req.Name) + + // clean the object up if it's being deleted + if !obj.DeletionTimestamp.IsZero() && time.Now().After(obj.DeletionTimestamp.Time) { + log.V(util.DebugLevel).Info("Resource is being deleted, its configuration will be removed", "type", "KongVault", "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 // wait until the object is no longer present in the cache + } + return ctrl.Result{}, nil + } + + class := new(netv1.IngressClass) + if !r.DisableIngressClassLookups { + if err := r.Get(ctx, k8stypes.NamespacedName{Name: r.IngressClassName}, class); err != nil { + // we log this without taking action to support legacy configurations that only set ingressClassName or + // used the class annotation and did not create a corresponding IngressClass. We only need this to determine + // if the IngressClass is default or to configure default settings, and can assume no/no additional defaults + // if none exists. + log.V(util.DebugLevel).Info("Could not retrieve IngressClass", "ingressclass", r.IngressClassName) + } + } + // if the object is not configured with our ingress.class, then we need to ensure it's removed from the cache + if !ctrlutils.MatchesIngressClass(obj, r.IngressClassName, ctrlutils.IsDefaultIngressClass(class)) { + log.V(util.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) + } else { + log.V(util.DebugLevel).Info("Object has matching ingress class", "namespace", req.Namespace, "name", req.Name, + "class", r.IngressClassName) + } + + // update the kong Admin API with the changes + if err := r.DataplaneClient.UpdateObject(obj); err != nil { + return ctrl.Result{}, err + } + // if status updates are enabled report the status for the object + if r.DataplaneClient.AreKubernetesObjectReportsEnabled() { + log.V(util.DebugLevel).Info("Updating programmed condition status", "namespace", req.Namespace, "name", req.Name) + 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) + } + log.V(util.DebugLevel).Info("Status update not needed", "namespace", req.Namespace, "name", req.Name) + } + + return ctrl.Result{}, nil +} + // ----------------------------------------------------------------------------- // API Group "" resource nodes // ----------------------------------------------------------------------------- diff --git a/internal/dataplane/deckgen/generate.go b/internal/dataplane/deckgen/generate.go index 382bb1f7f5..8445c68e75 100644 --- a/internal/dataplane/deckgen/generate.go +++ b/internal/dataplane/deckgen/generate.go @@ -203,6 +203,18 @@ func ToDeckContent( sort.SliceStable(content.Consumers, func(i, j int) bool { return strings.Compare(*content.Consumers[i].Username, *content.Consumers[j].Username) > 0 }) + + // convert vaults. + for _, v := range k8sState.Vaults { + vault := file.FVault{ + Vault: v.Vault, + } + content.Vaults = append(content.Vaults, vault) + } + sort.SliceStable(content.Vaults, func(i, j int) bool { + return (*content.Vaults[i].Prefix) > (*content.Vaults[j].Prefix) + }) + if len(params.SelectorTags) > 0 { content.Info = &file.Info{ SelectorTags: params.SelectorTags, diff --git a/internal/dataplane/failures/failures.go b/internal/dataplane/failures/failures.go index b628a69909..1ba4eaf2d8 100644 --- a/internal/dataplane/failures/failures.go +++ b/internal/dataplane/failures/failures.go @@ -41,9 +41,11 @@ func NewResourceFailure(reason string, causingObjects ...client.Object) (Resourc if obj.GetName() == "" { return ResourceFailure{}, fmt.Errorf("one of causing objects (%s) has no name", gvk.String()) } + if obj.GetNamespace() == "" { return ResourceFailure{}, fmt.Errorf("one of causing objects (%s) has no namespace", gvk.String()) } + } return ResourceFailure{ diff --git a/internal/dataplane/kongstate/kongstate.go b/internal/dataplane/kongstate/kongstate.go index e582c95d42..18290319b6 100644 --- a/internal/dataplane/kongstate/kongstate.go +++ b/internal/dataplane/kongstate/kongstate.go @@ -31,6 +31,7 @@ type KongState struct { Plugins []Plugin Consumers []Consumer ConsumerGroups []ConsumerGroup + Vaults []Vault } // SanitizedCopy returns a shallow copy with sensitive values redacted best-effort. @@ -59,6 +60,7 @@ func (ks *KongState) SanitizedCopy() *KongState { return }(), ConsumerGroups: ks.ConsumerGroups, + Vaults: ks.Vaults, } } @@ -239,6 +241,34 @@ func (ks *KongState) FillUpstreamOverrides( } } +func (ks *KongState) FillVaults( + logger logr.Logger, + s store.Storer, + failuresCollector *failures.ResourceFailuresCollector, +) { + for _, vault := range s.ListKongVaults() { + config, err := RawConfigToConfiguration(vault.Spec.Config.Raw) + if err != nil { + logger.Error(err, "failed to parse configuration of vault to JSON", "name", vault.Name) + failuresCollector.PushResourceFailure( + fmt.Sprintf("failed to parse configuration of vault %s to JSON: %v", vault.Name, err), + vault, + ) + continue + } + ks.Vaults = append(ks.Vaults, Vault{ + Vault: kong.Vault{ + Name: kong.String(vault.Spec.Backend), + Description: kong.String(vault.Spec.Description), + Prefix: kong.String(vault.Spec.Prefix), + Config: config, + Tags: util.GenerateTagsForObject(vault), + }, + K8sKongVault: vault.DeepCopy(), + }) + } +} + func (ks *KongState) getPluginRelations() map[string]util.ForeignRelations { // KongPlugin key (KongPlugin's name:namespace) to corresponding associations pluginRels := map[string]util.ForeignRelations{} @@ -487,6 +517,14 @@ func (ks *KongState) FillIDs(logger logr.Logger) { ks.ConsumerGroups[consumerGroupIndex] = consumerGroup } } + + for valutIndex, vault := range ks.Vaults { + if err := vault.FillID(); err != nil { + logger.Error(err, "Failed to fill ID for vault", "vault_name", vault.FriendlyName()) + } else { + ks.Vaults[valutIndex] = vault + } + } } // maybeLogKongIngressDeprecationError iterates over services and logs a deprecation error if a service diff --git a/internal/dataplane/kongstate/kongstate_test.go b/internal/dataplane/kongstate/kongstate_test.go index 90c069e38b..bf2a7b2c98 100644 --- a/internal/dataplane/kongstate/kongstate_test.go +++ b/internal/dataplane/kongstate/kongstate_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/go-logr/logr" + "github.com/go-logr/logr/testr" "github.com/go-logr/zapr" "github.com/kong/go-kong/kong" "github.com/samber/lo" @@ -25,6 +26,7 @@ import ( "github.com/kong/kubernetes-ingress-controller/v3/internal/store" "github.com/kong/kubernetes-ingress-controller/v3/internal/util" kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" kongv1beta1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1beta1" ) @@ -55,6 +57,13 @@ func TestKongState_SanitizedCopy(t *testing.T) { ConsumerGroups: []ConsumerGroup{{ ConsumerGroup: kong.ConsumerGroup{ID: kong.String("1"), Name: kong.String("consumer-group")}, }}, + Vaults: []Vault{ + { + Vault: kong.Vault{ + Name: kong.String("test-vault"), Prefix: kong.String("test-vault"), + }, + }, + }, }, want: KongState{ Services: []Service{{Service: kong.Service{ID: kong.String("1")}}}, @@ -69,6 +78,13 @@ func TestKongState_SanitizedCopy(t *testing.T) { ConsumerGroups: []ConsumerGroup{{ ConsumerGroup: kong.ConsumerGroup{ID: kong.String("1"), Name: kong.String("consumer-group")}, }}, + Vaults: []Vault{ + { + Vault: kong.Vault{ + Name: kong.String("test-vault"), Prefix: kong.String("test-vault"), + }, + }, + }, }, }, } { @@ -909,6 +925,37 @@ func TestKongState_FillIDs(t *testing.T) { require.NotEmpty(t, s.Consumers[0].ID) }, }, + { + name: "fills consumer, consumer group, vault IDs", + state: KongState{ + Consumers: []Consumer{ + { + Consumer: kong.Consumer{ + Username: kong.String("user.0"), + }, + }, + }, + ConsumerGroups: []ConsumerGroup{ + { + ConsumerGroup: kong.ConsumerGroup{ + Name: kong.String("cg.0"), + }, + }, + }, + Vaults: []Vault{ + { + Vault: kong.Vault{ + Prefix: kong.String("vault.0"), + }, + }, + }, + }, + expect: func(t *testing.T, s KongState) { + require.NotEmpty(t, s.Consumers[0].ID) + require.NotEmpty(t, s.ConsumerGroups[0].ID) + require.NotEmpty(t, s.Vaults[0].ID) + }, + }, } for _, tc := range testCases { @@ -1140,3 +1187,123 @@ func TestKongState_FillUpstreamOverrides(t *testing.T) { }) } } + +func TestFillVaults(t *testing.T) { + kongVaultTypeMeta := metav1.TypeMeta{ + APIVersion: kongv1alpha1.GroupVersion.String(), + Kind: "KongVault", + } + testCases := []struct { + name string + kongVaults []*kongv1alpha1.KongVault + expectedTranslatedVaults []Vault + // name of KongVault -> failure message + expectedTranslationFailures map[string]string + }{ + { + name: "single valid KongVault", + kongVaults: []*kongv1alpha1.KongVault{ + { + TypeMeta: kongVaultTypeMeta, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "", + Name: "vault-1", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-1", + }, + }, + }, + expectedTranslatedVaults: []Vault{ + { + Vault: kong.Vault{ + Name: kong.String("env"), + Prefix: kong.String("env-1"), + }, + K8sKongVault: &kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + }, + }, + }, + }, + }, + { + name: "one valid KongVault with correct ingress class, and one KongVault with other ingress class", + kongVaults: []*kongv1alpha1.KongVault{ + { + TypeMeta: kongVaultTypeMeta, + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + Annotations: map[string]string{ + annotations.IngressClassKey: annotations.DefaultIngressClass, + }, + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-1", + }, + }, + { + TypeMeta: kongVaultTypeMeta, + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-2", + Annotations: map[string]string{ + annotations.IngressClassKey: "other-ingress-class", + }, + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "env-2", + }, + }, + }, + expectedTranslatedVaults: []Vault{ + { + Vault: kong.Vault{ + Name: kong.String("env"), + Prefix: kong.String("env-1"), + }, + K8sKongVault: &kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "vault-1", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + s, err := store.NewFakeStore(store.FakeObjects{ + KongVaults: tc.kongVaults, + }) + + require.NoError(t, err) + logger := testr.New(t) + f := failures.NewResourceFailuresCollector(logger) + ks := &KongState{} + ks.FillVaults(logger, s, f) + + assert.Len(t, ks.Vaults, len(tc.expectedTranslatedVaults), "should have expected number of translated vaults") + for _, expectedVault := range tc.expectedTranslatedVaults { + assert.Truef(t, lo.ContainsBy(ks.Vaults, func(v Vault) bool { + return (v.Name != nil && *v.Name == *expectedVault.Name) && + (v.Prefix != nil && *v.Prefix == *expectedVault.Prefix) && + (v.K8sKongVault != nil && v.K8sKongVault.Name == expectedVault.K8sKongVault.Name) + }), + "cannot find translated vault for KongVault %q", expectedVault.K8sKongVault.Name, + ) + } + + // TODO: check translation failures after we implement translation failure events for cluster scoped objects: + // https://github.com/Kong/kubernetes-ingress-controller/issues/5387 + }) + } +} diff --git a/internal/dataplane/kongstate/plugin.go b/internal/dataplane/kongstate/plugin.go index 0c0de55bbf..56b7465882 100644 --- a/internal/dataplane/kongstate/plugin.go +++ b/internal/dataplane/kongstate/plugin.go @@ -223,7 +223,7 @@ func RawConfigurationWithPatchesToConfiguration( return kong.Configuration{}, err } } - return rawConfigToConfiguration(raw) + return RawConfigToConfiguration(raw) } // RawConfigurationWithNamespacedPatchesToConfiguration converts config and add patches from configPatches of KongClusterPlugin. @@ -253,21 +253,7 @@ func RawConfigurationWithNamespacedPatchesToConfiguration( return kong.Configuration{}, err } } - return rawConfigToConfiguration(raw) -} - -// rawConfigToConfiguration decodes raw JSON to the format of Kong configuration. -// it is run after all patches applied to the initial config. -func rawConfigToConfiguration(raw []byte) (kong.Configuration, error) { - if len(raw) == 0 { - return kong.Configuration{}, nil - } - var kongConfig kong.Configuration - err := json.Unmarshal(raw, &kongConfig) - if err != nil { - return kong.Configuration{}, err - } - return kongConfig, nil + return RawConfigToConfiguration(raw) } // NamespacedSecretToConfiguration fetches specified value from given namespace, secret and key, diff --git a/internal/dataplane/kongstate/util.go b/internal/dataplane/kongstate/util.go index 471fde2e65..f5ef152a79 100644 --- a/internal/dataplane/kongstate/util.go +++ b/internal/dataplane/kongstate/util.go @@ -1,9 +1,11 @@ package kongstate import ( + "encoding/json" "fmt" "strings" + "github.com/kong/go-kong/kong" corev1 "k8s.io/api/core/v1" "github.com/kong/kubernetes-ingress-controller/v3/internal/annotations" @@ -83,3 +85,17 @@ func prettyPrintServiceList(services []*corev1.Service) string { } return strings.Join(serviceList, ", ") } + +// RawConfigToConfiguration decodes raw JSON to the format of Kong configuration. +// it is run after all patches applied to the initial config. +func RawConfigToConfiguration(raw []byte) (kong.Configuration, error) { + if len(raw) == 0 { + return kong.Configuration{}, nil + } + var kongConfig kong.Configuration + err := json.Unmarshal(raw, &kongConfig) + if err != nil { + return kong.Configuration{}, err + } + return kongConfig, nil +} diff --git a/internal/dataplane/kongstate/vault.go b/internal/dataplane/kongstate/vault.go new file mode 100644 index 0000000000..4a53d9a0fb --- /dev/null +++ b/internal/dataplane/kongstate/vault.go @@ -0,0 +1,13 @@ +package kongstate + +import ( + "github.com/kong/go-kong/kong" + + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" +) + +type Vault struct { + kong.Vault + + K8sKongVault *kongv1alpha1.KongVault +} diff --git a/internal/dataplane/translator/translator.go b/internal/dataplane/translator/translator.go index ba239733d9..41eb09d3c2 100644 --- a/internal/dataplane/translator/translator.go +++ b/internal/dataplane/translator/translator.go @@ -180,6 +180,12 @@ func (t *Translator) BuildKongConfig() KongConfigBuildingResult { t.registerSuccessfullyTranslatedObject(&result.Consumers[i].K8sKongConsumer) } + // generate vaults + result.FillVaults(t.logger, t.storer, t.failuresCollector) + for i := range result.Vaults { + t.registerSuccessfullyTranslatedObject(result.Vaults[i].K8sKongVault) + } + // process consumer groups result.FillConsumerGroups(t.logger, t.storer) for i := range result.ConsumerGroups { diff --git a/internal/manager/config.go b/internal/manager/config.go index 5122be0935..ae40c81b9e 100644 --- a/internal/manager/config.go +++ b/internal/manager/config.go @@ -109,6 +109,7 @@ type Config struct { ServiceEnabled bool KongUpstreamPolicyEnabled bool KongServiceFacadeEnabled bool + KongVaultEnabled bool // Gateway API toggling. GatewayAPIGatewayController bool @@ -249,6 +250,7 @@ func (c *Config) FlagSet() *pflag.FlagSet { flagSet.BoolVar(&c.GatewayAPIHTTPRouteController, "enable-controller-gwapi-httproute", true, "Enable the Gateway API HTTPRoute controller.") flagSet.BoolVar(&c.GatewayAPIReferenceGrantController, "enable-controller-gwapi-reference-grant", true, "Enable the Gateway API ReferenceGrant controller.") flagSet.BoolVar(&c.KongServiceFacadeEnabled, "enable-controller-kong-service-facade", true, "Enable the KongServiceFacade controller.") + flagSet.BoolVar(&c.KongVaultEnabled, "enable-controller-kong-vault", true, "Enable the KongVault controller.") // Admission Webhook server config flagSet.StringVar(&c.AdmissionServer.ListenAddr, "admission-webhook-listen", "off", diff --git a/internal/manager/controllerdef.go b/internal/manager/controllerdef.go index 5c64fcdedb..ed201983ea 100644 --- a/internal/manager/controllerdef.go +++ b/internal/manager/controllerdef.go @@ -285,6 +285,19 @@ func setupControllers( StatusQueue: kubernetesStatusQueue, }, }, + { + Enabled: c.KongVaultEnabled, + Controller: &configuration.KongV1Alpha1KongVaultReconciler{ + Client: mgr.GetClient(), + Log: ctrl.LoggerFrom(ctx).WithName("controllers").WithName("KongVault"), + Scheme: mgr.GetScheme(), + DataplaneClient: dataplaneClient, + CacheSyncTimeout: c.CacheSyncTimeout, + IngressClassName: c.IngressClassName, + DisableIngressClassLookups: !c.IngressClassNetV1Enabled, + StatusQueue: kubernetesStatusQueue, + }, + }, // --------------------------------------------------------------------------- // Gateway API Controllers // --------------------------------------------------------------------------- diff --git a/internal/store/cache_stores.go b/internal/store/cache_stores.go index 289e526d9c..a4483fb2c2 100644 --- a/internal/store/cache_stores.go +++ b/internal/store/cache_stores.go @@ -50,6 +50,7 @@ type CacheStores struct { KongUpstreamPolicy cache.Store IngressClassParametersV1alpha1 cache.Store KongServiceFacade cache.Store + KongVault cache.Store l *sync.RWMutex } @@ -82,6 +83,7 @@ func NewCacheStores() CacheStores { KongUpstreamPolicy: cache.NewStore(keyFunc), IngressClassParametersV1alpha1: cache.NewStore(keyFunc), KongServiceFacade: cache.NewStore(keyFunc), + KongVault: cache.NewStore(clusterResourceKeyFunc), l: &sync.RWMutex{}, } @@ -188,6 +190,8 @@ func (c CacheStores) Get(obj runtime.Object) (item interface{}, exists bool, err return c.IngressClassParametersV1alpha1.Get(obj) case *incubatorv1alpha1.KongServiceFacade: return c.KongServiceFacade.Get(obj) + case *kongv1alpha1.KongVault: + return c.KongVault.Get(obj) } return nil, false, fmt.Errorf("%T is not a supported cache object type", obj) } @@ -252,6 +256,8 @@ func (c CacheStores) Add(obj runtime.Object) error { return c.IngressClassParametersV1alpha1.Add(obj) case *incubatorv1alpha1.KongServiceFacade: return c.KongServiceFacade.Add(obj) + case *kongv1alpha1.KongVault: + return c.KongVault.Add(obj) default: return fmt.Errorf("cannot add unsupported kind %q to the store", obj.GetObjectKind().GroupVersionKind()) } @@ -317,6 +323,8 @@ func (c CacheStores) Delete(obj runtime.Object) error { return c.IngressClassParametersV1alpha1.Delete(obj) case *incubatorv1alpha1.KongServiceFacade: return c.KongServiceFacade.Delete(obj) + case *kongv1alpha1.KongVault: + return c.KongVault.Delete(obj) default: return fmt.Errorf("cannot delete unsupported kind %q from the store", obj.GetObjectKind().GroupVersionKind()) } diff --git a/internal/store/fake_store.go b/internal/store/fake_store.go index 11e0ef1343..8df30fd15a 100644 --- a/internal/store/fake_store.go +++ b/internal/store/fake_store.go @@ -64,6 +64,7 @@ type FakeObjects struct { KongConsumerGroups []*kongv1beta1.KongConsumerGroup KongUpstreamPolicies []*kongv1beta1.KongUpstreamPolicy KongServiceFacades []*incubatorv1alpha1.KongServiceFacade + KongVaults []*kongv1alpha1.KongVault } // NewFakeStore creates a store backed by the objects passed in as arguments. @@ -218,6 +219,13 @@ func NewFakeStore( return nil, err } } + kongVaultStore := cache.NewStore(clusterResourceKeyFunc) + for _, v := range objects.KongVaults { + err := kongVaultStore.Add(v) + if err != nil { + return nil, err + } + } s = Store{ stores: CacheStores{ @@ -243,6 +251,7 @@ func NewFakeStore( IngressClassParametersV1alpha1: IngressClassParametersV1alpha1Store, KongUpstreamPolicy: kongUpstreamPolicyStore, KongServiceFacade: kongServiceFacade, + KongVault: kongVaultStore, }, ingressClass: annotations.DefaultIngressClass, isValidIngressClass: annotations.IngressClassValidatorFuncFromObjectMeta(annotations.DefaultIngressClass), @@ -279,6 +288,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) { reflect.TypeOf(&kongv1.KongIngress{}): kongv1.SchemeGroupVersion.WithKind("KongIngress"), reflect.TypeOf(&kongv1.KongConsumer{}): kongv1.SchemeGroupVersion.WithKind("KongConsumer"), reflect.TypeOf(&kongv1beta1.KongConsumerGroup{}): kongv1beta1.SchemeGroupVersion.WithKind("KongConsumerGroup"), + reflect.TypeOf(&kongv1alpha1.KongVault{}): kongv1alpha1.SchemeGroupVersion.WithKind(kongv1alpha1.KongVaultKind), } out := &bytes.Buffer{} @@ -321,6 +331,7 @@ func (objects FakeObjects) MarshalToYAML() ([]byte, error) { allObjects = append(allObjects, lo.ToAnySlice(objects.KongIngresses)...) allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumers)...) allObjects = append(allObjects, lo.ToAnySlice(objects.KongConsumerGroups)...) + allObjects = append(allObjects, lo.ToAnySlice(objects.KongVaults)...) for _, obj := range allObjects { if err := fillGVKAndAppendToBuffer(obj.(runtime.Object)); err != nil { diff --git a/internal/store/store.go b/internal/store/store.go index b7dea5a348..932397e716 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -69,6 +69,7 @@ type Storer interface { GetGateway(namespace string, name string) (*gatewayapi.Gateway, error) GetKongUpstreamPolicy(namespace, name string) (*kongv1beta1.KongUpstreamPolicy, error) GetKongServiceFacade(namespace, name string) (*incubatorv1alpha1.KongServiceFacade, error) + GetKongVault(name string) (*kongv1alpha1.KongVault, error) ListIngressesV1() []*netv1.Ingress ListIngressClassesV1() []*netv1.IngressClass @@ -88,6 +89,7 @@ type Storer interface { ListKongConsumers() []*kongv1.KongConsumer ListKongConsumerGroups() []*kongv1beta1.KongConsumerGroup ListCACerts() ([]*corev1.Secret, error) + ListKongVaults() []*kongv1alpha1.KongVault } // Store implements Storer and can be used to list Ingress, Services @@ -571,6 +573,18 @@ func (s Store) GetGateway(namespace string, name string) (*gatewayapi.Gateway, e return obj.(*gatewayapi.Gateway), nil } +// GetKongVault returns kongvault resource having specified name. +func (s Store) GetKongVault(name string) (*kongv1alpha1.KongVault, error) { + p, exists, err := s.stores.KongVault.GetByKey(name) + if err != nil { + return nil, err + } + if !exists { + return nil, NotFoundError{fmt.Sprintf("KongVault %v not found", name)} + } + return p.(*kongv1alpha1.KongVault), nil +} + // ListKongConsumers returns all KongConsumers filtered by the ingress.class // annotation. func (s Store) ListKongConsumers() []*kongv1.KongConsumer { @@ -670,6 +684,17 @@ func (s Store) ListCACerts() ([]*corev1.Secret, error) { return secrets, nil } +func (s Store) ListKongVaults() []*kongv1alpha1.KongVault { + var kongVaults []*kongv1alpha1.KongVault + for _, obj := range s.stores.KongVault.List() { + kongVault, ok := obj.(*kongv1alpha1.KongVault) + if ok && s.isValidIngressClass(&kongVault.ObjectMeta, annotations.IngressClassKey, s.getIngressClassHandling()) { + kongVaults = append(kongVaults, kongVault) + } + } + return kongVaults +} + // 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/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml index 117abe0bd4..1acbbc511c 100644 --- a/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-dbless-k4k8s-enterprise.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml index 688c3f2d63..9c7f36a143 100644 --- a/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-dbless-konnect-enterprise.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-dbless-konnect.yaml b/test/e2e/manifests/all-in-one-dbless-konnect.yaml index c52ce3400b..81dc2c9d12 100644 --- a/test/e2e/manifests/all-in-one-dbless-konnect.yaml +++ b/test/e2e/manifests/all-in-one-dbless-konnect.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-dbless.yaml b/test/e2e/manifests/all-in-one-dbless.yaml index b96fde0160..5ce0728a7b 100644 --- a/test/e2e/manifests/all-in-one-dbless.yaml +++ b/test/e2e/manifests/all-in-one-dbless.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml index 3ecd9a22b4..e419a35359 100644 --- a/test/e2e/manifests/all-in-one-postgres-enterprise.yaml +++ b/test/e2e/manifests/all-in-one-postgres-enterprise.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml index de95096ea6..da4e0f37b9 100644 --- a/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml +++ b/test/e2e/manifests/all-in-one-postgres-multiple-gateways.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/e2e/manifests/all-in-one-postgres.yaml b/test/e2e/manifests/all-in-one-postgres.yaml index 9b239680fa..060959a1e5 100644 --- a/test/e2e/manifests/all-in-one-postgres.yaml +++ b/test/e2e/manifests/all-in-one-postgres.yaml @@ -1981,6 +1981,184 @@ spec: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: kongvaults.configuration.konghq.com +spec: + group: configuration.konghq.com + names: + categories: + - kong-ingress-controller + kind: KongVault + listKind: KongVaultList + plural: kongvaults + shortNames: + - kv + singular: kongvault + scope: Cluster + versions: + - additionalPrinterColumns: + - description: Name of the backend of the vault + jsonPath: .spec.backend + name: Backend Type + type: string + - description: Prefix of vault URI to reference the values in the vault + jsonPath: .spec.prefix + name: Prefix + type: string + - description: Age + jsonPath: .metadata.creationTimestamp + name: Age + type: date + - description: Description + jsonPath: .spec.description + name: Description + priority: 1 + type: string + - jsonPath: .status.conditions[?(@.type=="Programmed")].status + name: Programmed + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: 'KongVault is the schema for kongvaults API which defines a custom + Kong vault. A Kong vault is a storage to store sensitive data, where the + values can be referenced in configuration of plugins. See: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/' + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: KongVaultSpec defines specification of a custom Kong vault. + properties: + backend: + description: 'Backend is the type of the backend storing the secrets + in the vault. The supported backends of Kong is listed here: https://docs.konghq.com/gateway/latest/kong-enterprise/secrets-management/backends/' + minLength: 1 + type: string + config: + description: Config is the configuration of the vault. Varies for + different backends. + x-kubernetes-preserve-unknown-fields: true + description: + description: Description is the additional information about the vault. + type: string + prefix: + description: Prefix is the prefix of vault URI for referencing values + in the vault. + minLength: 1 + type: string + required: + - backend + - prefix + type: object + status: + description: KongVaultStatus represents the current status of the KongVault + resource. + properties: + conditions: + default: + - lastTransitionTime: "1970-01-01T00:00:00Z" + message: Waiting for controller + reason: Pending + status: Unknown + type: Programmed + description: "Conditions describe the current conditions of the KongVaultStatus. + \n Known condition types are: \n * \"Programmed\"" + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + maxItems: 8 + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + required: + - conditions + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.13.0 @@ -2538,6 +2716,22 @@ rules: - get - patch - update +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults + verbs: + - get + - list + - watch +- apiGroups: + - configuration.konghq.com + resources: + - kongvaults/status + verbs: + - get + - patch + - update - apiGroups: - configuration.konghq.com resources: diff --git a/test/integration/suite_test.go b/test/integration/suite_test.go index 69bc0251c0..ec6a3e9d81 100644 --- a/test/integration/suite_test.go +++ b/test/integration/suite_test.go @@ -60,6 +60,8 @@ func TestMain(m *testing.M) { if testenv.KongImage() != "" && testenv.KongTag() != "" { fmt.Printf("INFO: custom kong image specified via env: %s:%s\n", testenv.KongImage(), testenv.KongTag()) } + // add env for vaults. + kongbuilder.WithProxyEnvVar("vault_test_add_header_1", "h1:v1") // Pin the Helm chart version. kongbuilder.WithHelmChartVersion(testenv.KongHelmChartVersion()) diff --git a/test/integration/vault_test.go b/test/integration/vault_test.go new file mode 100644 index 0000000000..3b346cb9f3 --- /dev/null +++ b/test/integration/vault_test.go @@ -0,0 +1,141 @@ +//go:build integration_tests + +package integration + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/kong/go-kong/kong" + "github.com/kong/kubernetes-testing-framework/pkg/clusters" + "github.com/kong/kubernetes-testing-framework/pkg/utils/kubernetes/generators" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kong/kubernetes-ingress-controller/v3/internal/annotations" + dpconf "github.com/kong/kubernetes-ingress-controller/v3/internal/dataplane/config" + kongv1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1" + kongv1alpha1 "github.com/kong/kubernetes-ingress-controller/v3/pkg/apis/configuration/v1alpha1" + "github.com/kong/kubernetes-ingress-controller/v3/pkg/clientset" + "github.com/kong/kubernetes-ingress-controller/v3/test" + "github.com/kong/kubernetes-ingress-controller/v3/test/consts" + "github.com/kong/kubernetes-ingress-controller/v3/test/internal/helpers" +) + +func TestCustomVault(t *testing.T) { + t.Parallel() + + RunWhenKongEnterprise(t) + // TODO: run hcv vault to enable test with DBMode + RunWhenKongDBMode(t, dpconf.DBModeOff, "Skipping because DBMode cannot support env vault") + + ctx := context.Background() + ns, cleaner := helpers.Setup(ctx, t, env) + + t.Log("deploying a minimal HTTP container deployment to test Ingress routes") + container := generators.NewContainer("httpbin", test.HTTPBinImage, test.HTTPBinPort) + deployment := generators.NewDeploymentForContainer(container) + deployment, err := env.Cluster().Client().AppsV1().Deployments(ns.Name).Create(ctx, deployment, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(deployment) + + t.Logf("exposing deployment %s via service", deployment.Name) + service := generators.NewServiceForDeployment(deployment, corev1.ServiceTypeLoadBalancer) + _, err = env.Cluster().Client().CoreV1().Services(ns.Name).Create(ctx, service, metav1.CreateOptions{}) + require.NoError(t, err) + cleaner.Add(service) + + t.Logf("creating an ingress for service %s with ingress.class %s", service.Name, consts.IngressClass) + ingress := generators.NewIngressForService("/test_custom_vault", map[string]string{ + "konghq.com/strip-path": "true", + }, service) + ingress.Spec.IngressClassName = kong.String(consts.IngressClass) + require.NoError(t, clusters.DeployIngress(ctx, env.Cluster(), ns.Name, ingress)) + cleaner.Add(ingress) + + t.Log("waiting for routes from Ingress to be operational") + require.Eventually(t, func() bool { + resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_custom_vault", proxyURL)) + if err != nil { + t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err) + return false + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + b := new(bytes.Buffer) + n, err := b.ReadFrom(resp.Body) + require.NoError(t, err) + require.True(t, n > 0) + return strings.Contains(b.String(), "httpbin.org") + } + return false + }, ingressWait, waitTick) + + t.Logf("creating a Kong vault using env backend") + + c, err := clientset.NewForConfig(env.Cluster().Config()) + require.NoError(t, err) + + _, err = c.ConfigurationV1alpha1().KongVaults().Create(ctx, &kongv1alpha1.KongVault{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-env-vault", + Annotations: map[string]string{ + annotations.IngressClassKey: consts.IngressClass, + }, + }, + Spec: kongv1alpha1.KongVaultSpec{ + Backend: "env", + Prefix: "test-env", + Description: "env vault for test", + Config: apiextensionsv1.JSON{ + Raw: []byte(`{"prefix":"kong_vault_test_"}`), + }, + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("create a request-transformer-advanced plugin referencing the value from the vault") + _, err = c.ConfigurationV1().KongPlugins(ns.Name).Create(ctx, &kongv1.KongPlugin{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: ns.Name, + Name: "request-transformer-advanced", + }, + PluginName: "request-transformer-advanced", + Config: apiextensionsv1.JSON{ + Raw: []byte(`{"add":{"headers":["{vault://test-env/add-header-1}"]}}`), + }, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + t.Logf("attach plugin to ingress and check if the config from vault takes effect") + ingress, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Get(ctx, ingress.Name, metav1.GetOptions{}) + require.NoError(t, err) + ingress.Annotations[annotations.AnnotationPrefix+annotations.PluginsKey] = "request-transformer-advanced" + _, err = env.Cluster().Client().NetworkingV1().Ingresses(ns.Name).Update(ctx, ingress, metav1.UpdateOptions{}) + require.NoError(t, err) + require.Eventuallyf(t, func() bool { + resp, err := helpers.DefaultHTTPClient().Get(fmt.Sprintf("%s/test_custom_vault/headers", proxyURL)) + if err != nil { + t.Logf("WARNING: error while waiting for %s: %v", proxyURL, err) + return false + } + defer resp.Body.Close() + if resp.StatusCode == http.StatusOK { + b := new(bytes.Buffer) + n, err := b.ReadFrom(resp.Body) + require.NoError(t, err) + require.True(t, n > 0) + return strings.Contains(b.String(), `"H1": "v1"`) + } + return false + }, + ingressWait, waitTick, + "Cannot find added header in request", + ) +}