From 477b167bf97a03099cd40ca6b5539ab9ccf8df4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Wed, 18 Sep 2024 20:40:48 +0200 Subject: [PATCH] feat(konnect): add KongCredentialSecretReconciler to reconcile consumer Secrets and create Credential resources in response --- .mockery.yaml | 1 + config/rbac/role/role.yaml | 20 ++ controller/konnect/constraints/constraints.go | 1 + controller/konnect/index.go | 7 +- .../konnect/index_credentials_basicauth.go | 48 +++ controller/konnect/index_kongconsumer.go | 32 ++ .../konnect/ops/credenetialbasicauth.go | 14 + .../konnect/ops/credenetialbasicauth_mock.go | 259 ++++++++++++++++ controller/konnect/ops/sdkfactory.go | 5 + .../konnect/reconciler_credential_secrets.go | 283 ++++++++++++++++++ .../reconciler_credential_secrets_rbac.go | 7 + controller/konnect/reconciler_generic_rbac.go | 6 + go.mod | 2 +- go.sum | 4 +- modules/manager/controller_setup.go | 18 ++ 15 files changed, 703 insertions(+), 4 deletions(-) create mode 100644 controller/konnect/index_credentials_basicauth.go create mode 100644 controller/konnect/index_kongconsumer.go create mode 100644 controller/konnect/ops/credenetialbasicauth.go create mode 100644 controller/konnect/ops/credenetialbasicauth_mock.go create mode 100644 controller/konnect/reconciler_credential_secrets.go create mode 100644 controller/konnect/reconciler_credential_secrets_rbac.go diff --git a/.mockery.yaml b/.mockery.yaml index dc63c499b..960e58e76 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -19,3 +19,4 @@ packages: PluginSDK: UpstreamsSDK: MeSDK: + CredentialBasicAuthSDK: diff --git a/config/rbac/role/role.yaml b/config/rbac/role/role.yaml index 4a608fc44..fb857b34b 100644 --- a/config/rbac/role/role.yaml +++ b/config/rbac/role/role.yaml @@ -332,6 +332,26 @@ rules: - get - patch - update +- apiGroups: + - konnect.konghq.com + resources: + - credentialbasicauths + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - konnect.konghq.com + resources: + - kongconsumers + verbs: + - get + - list + - watch - apiGroups: - konnect.konghq.com resources: diff --git a/controller/konnect/constraints/constraints.go b/controller/konnect/constraints/constraints.go index 57790028c..08f16be5c 100644 --- a/controller/konnect/constraints/constraints.go +++ b/controller/konnect/constraints/constraints.go @@ -19,6 +19,7 @@ type SupportedKonnectEntityType interface { configurationv1.KongConsumer | configurationv1beta1.KongConsumerGroup | configurationv1alpha1.KongPluginBinding | + configurationv1alpha1.CredentialBasicAuth | configurationv1alpha1.KongUpstream // TODO: add other types diff --git a/controller/konnect/index.go b/controller/konnect/index.go index 9719a65ba..a73c777d6 100644 --- a/controller/konnect/index.go +++ b/controller/konnect/index.go @@ -5,6 +5,7 @@ import ( "github.com/kong/gateway-operator/controller/konnect/constraints" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" ) @@ -21,9 +22,13 @@ func ReconciliationIndexOptionsForEntity[ T constraints.SupportedKonnectEntityType, ]() []ReconciliationIndexOption { var e TEnt - switch any(e).(type) { //nolint:gocritic // TODO: add index options required for other entities + switch any(e).(type) { case *configurationv1alpha1.KongPluginBinding: return IndexOptionsForKongPluginBinding() + case *configurationv1.KongConsumer: + return IndexOptionsForKongConsumer() + case *configurationv1alpha1.CredentialBasicAuth: + return IndexOptionsForCredentialsBasicAuth() } return nil } diff --git a/controller/konnect/index_credentials_basicauth.go b/controller/konnect/index_credentials_basicauth.go new file mode 100644 index 000000000..77fffb9a8 --- /dev/null +++ b/controller/konnect/index_credentials_basicauth.go @@ -0,0 +1,48 @@ +package konnect + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +const ( + // IndexFieldCredentialReferencesKongConsumer is the index name for CredentialBasicAuth -> Consumer. + IndexFieldCredentialReferencesKongConsumer = "kongCredentialsBasicAuthConsumerRef" + // IndexFieldCredentialReferencesKongSecret is the index name for CredentialBasicAuth -> Secret. + IndexFieldCredentialReferencesKongSecret = "kongCredentialsBasicAuthSecretRef" +) + +// IndexOptionsForCredentialsBasicAuth returns required Index options for CredentialBasicAuth. +func IndexOptionsForCredentialsBasicAuth() []ReconciliationIndexOption { + return []ReconciliationIndexOption{ + { + IndexObject: &configurationv1alpha1.CredentialBasicAuth{}, + IndexField: IndexFieldCredentialReferencesKongConsumer, + ExtractValue: kongCredentialBasicAuthReferencesConsumer, + }, + { + IndexObject: &configurationv1alpha1.CredentialBasicAuth{}, + IndexField: IndexFieldCredentialReferencesKongSecret, + ExtractValue: kongCredentialBasicAuthReferencesSecret, + }, + } +} + +// kongCredentialBasicAuthReferencesConsumer returns the name of referenced Consumer. +func kongCredentialBasicAuthReferencesConsumer(obj client.Object) []string { + cred, ok := obj.(*configurationv1alpha1.CredentialBasicAuth) + if !ok { + return nil + } + return []string{cred.Spec.ConsumerRef.Name} +} + +// kongCredentialBasicAuthReferencesSecret returns the name of referenced Secret. +func kongCredentialBasicAuthReferencesSecret(obj client.Object) []string { + cred, ok := obj.(*configurationv1alpha1.CredentialBasicAuth) + if !ok { + return nil + } + return []string{cred.Spec.SecretRef.Name} +} diff --git a/controller/konnect/index_kongconsumer.go b/controller/konnect/index_kongconsumer.go new file mode 100644 index 000000000..3a3c101ff --- /dev/null +++ b/controller/konnect/index_kongconsumer.go @@ -0,0 +1,32 @@ +package konnect + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" +) + +const ( + // IndexFieldKongConsumerReferencesSecrets is the index field for Consumer -> Secret. + IndexFieldKongConsumerReferencesSecrets = "kongConsumerSecretRef" +) + +// IndexOptionsForKongConsumer returns required Index options for Kong Consumer. +func IndexOptionsForKongConsumer() []ReconciliationIndexOption { + return []ReconciliationIndexOption{ + { + IndexObject: &configurationv1.KongConsumer{}, + IndexField: IndexFieldKongConsumerReferencesSecrets, + ExtractValue: kongConsumerReferencesSecret, + }, + } +} + +// kongConsumerReferencesSecret returns name of referenced Secrets. +func kongConsumerReferencesSecret(obj client.Object) []string { + consumer, ok := obj.(*configurationv1.KongConsumer) + if !ok { + return nil + } + return consumer.Credentials +} diff --git a/controller/konnect/ops/credenetialbasicauth.go b/controller/konnect/ops/credenetialbasicauth.go new file mode 100644 index 000000000..9c94239c9 --- /dev/null +++ b/controller/konnect/ops/credenetialbasicauth.go @@ -0,0 +1,14 @@ +package ops + +import ( + "context" + + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// CredentialBasicAuthSDK is the interface for the Konnect CredentialBasicAuthSDK. +type CredentialBasicAuthSDK interface { + CreateBasicAuthWithConsumer(ctx context.Context, req sdkkonnectops.CreateBasicAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateBasicAuthWithConsumerResponse, error) + DeleteBasicAuthWithConsumer(ctx context.Context, request sdkkonnectops.DeleteBasicAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteBasicAuthWithConsumerResponse, error) + UpsertBasicAuthWithConsumer(ctx context.Context, request sdkkonnectops.UpsertBasicAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertBasicAuthWithConsumerResponse, error) +} diff --git a/controller/konnect/ops/credenetialbasicauth_mock.go b/controller/konnect/ops/credenetialbasicauth_mock.go new file mode 100644 index 000000000..cd8ebc737 --- /dev/null +++ b/controller/konnect/ops/credenetialbasicauth_mock.go @@ -0,0 +1,259 @@ +// Code generated by mockery. DO NOT EDIT. + +package ops + +import ( + context "context" + + operations "github.com/Kong/sdk-konnect-go/models/operations" + mock "github.com/stretchr/testify/mock" +) + +// MockCredentialBasicAuthSDK is an autogenerated mock type for the CredentialBasicAuthSDK type +type MockCredentialBasicAuthSDK struct { + mock.Mock +} + +type MockCredentialBasicAuthSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockCredentialBasicAuthSDK) EXPECT() *MockCredentialBasicAuthSDK_Expecter { + return &MockCredentialBasicAuthSDK_Expecter{mock: &_m.Mock} +} + +// CreateBasicAuthWithConsumer provides a mock function with given fields: ctx, req, opts +func (_m *MockCredentialBasicAuthSDK) CreateBasicAuthWithConsumer(ctx context.Context, req operations.CreateBasicAuthWithConsumerRequest, opts ...operations.Option) (*operations.CreateBasicAuthWithConsumerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateBasicAuthWithConsumer") + } + + var r0 *operations.CreateBasicAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.CreateBasicAuthWithConsumerRequest, ...operations.Option) (*operations.CreateBasicAuthWithConsumerResponse, error)); ok { + return rf(ctx, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.CreateBasicAuthWithConsumerRequest, ...operations.Option) *operations.CreateBasicAuthWithConsumerResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateBasicAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.CreateBasicAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateBasicAuthWithConsumer' +type MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call struct { + *mock.Call +} + +// CreateBasicAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - req operations.CreateBasicAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockCredentialBasicAuthSDK_Expecter) CreateBasicAuthWithConsumer(ctx interface{}, req interface{}, opts ...interface{}) *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call { + return &MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call{Call: _e.mock.On("CreateBasicAuthWithConsumer", + append([]interface{}{ctx, req}, opts...)...)} +} + +func (_c *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call) Run(run func(ctx context.Context, req operations.CreateBasicAuthWithConsumerRequest, opts ...operations.Option)) *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(operations.CreateBasicAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call) Return(_a0 *operations.CreateBasicAuthWithConsumerResponse, _a1 error) *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.CreateBasicAuthWithConsumerRequest, ...operations.Option) (*operations.CreateBasicAuthWithConsumerResponse, error)) *MockCredentialBasicAuthSDK_CreateBasicAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// DeleteBasicAuthWithConsumer provides a mock function with given fields: ctx, request, opts +func (_m *MockCredentialBasicAuthSDK) DeleteBasicAuthWithConsumer(ctx context.Context, request operations.DeleteBasicAuthWithConsumerRequest, opts ...operations.Option) (*operations.DeleteBasicAuthWithConsumerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, request) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteBasicAuthWithConsumer") + } + + var r0 *operations.DeleteBasicAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.DeleteBasicAuthWithConsumerRequest, ...operations.Option) (*operations.DeleteBasicAuthWithConsumerResponse, error)); ok { + return rf(ctx, request, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.DeleteBasicAuthWithConsumerRequest, ...operations.Option) *operations.DeleteBasicAuthWithConsumerResponse); ok { + r0 = rf(ctx, request, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteBasicAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.DeleteBasicAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, request, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteBasicAuthWithConsumer' +type MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call struct { + *mock.Call +} + +// DeleteBasicAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - request operations.DeleteBasicAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockCredentialBasicAuthSDK_Expecter) DeleteBasicAuthWithConsumer(ctx interface{}, request interface{}, opts ...interface{}) *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call { + return &MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call{Call: _e.mock.On("DeleteBasicAuthWithConsumer", + append([]interface{}{ctx, request}, opts...)...)} +} + +func (_c *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call) Run(run func(ctx context.Context, request operations.DeleteBasicAuthWithConsumerRequest, opts ...operations.Option)) *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(operations.DeleteBasicAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call) Return(_a0 *operations.DeleteBasicAuthWithConsumerResponse, _a1 error) *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.DeleteBasicAuthWithConsumerRequest, ...operations.Option) (*operations.DeleteBasicAuthWithConsumerResponse, error)) *MockCredentialBasicAuthSDK_DeleteBasicAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// UpsertBasicAuthWithConsumer provides a mock function with given fields: ctx, request, opts +func (_m *MockCredentialBasicAuthSDK) UpsertBasicAuthWithConsumer(ctx context.Context, request operations.UpsertBasicAuthWithConsumerRequest, opts ...operations.Option) (*operations.UpsertBasicAuthWithConsumerResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, request) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpsertBasicAuthWithConsumer") + } + + var r0 *operations.UpsertBasicAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertBasicAuthWithConsumerRequest, ...operations.Option) (*operations.UpsertBasicAuthWithConsumerResponse, error)); ok { + return rf(ctx, request, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertBasicAuthWithConsumerRequest, ...operations.Option) *operations.UpsertBasicAuthWithConsumerResponse); ok { + r0 = rf(ctx, request, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpsertBasicAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.UpsertBasicAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, request, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertBasicAuthWithConsumer' +type MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call struct { + *mock.Call +} + +// UpsertBasicAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - request operations.UpsertBasicAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockCredentialBasicAuthSDK_Expecter) UpsertBasicAuthWithConsumer(ctx interface{}, request interface{}, opts ...interface{}) *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call { + return &MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call{Call: _e.mock.On("UpsertBasicAuthWithConsumer", + append([]interface{}{ctx, request}, opts...)...)} +} + +func (_c *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call) Run(run func(ctx context.Context, request operations.UpsertBasicAuthWithConsumerRequest, opts ...operations.Option)) *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(operations.UpsertBasicAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call) Return(_a0 *operations.UpsertBasicAuthWithConsumerResponse, _a1 error) *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.UpsertBasicAuthWithConsumerRequest, ...operations.Option) (*operations.UpsertBasicAuthWithConsumerResponse, error)) *MockCredentialBasicAuthSDK_UpsertBasicAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// NewMockCredentialBasicAuthSDK creates a new instance of MockCredentialBasicAuthSDK. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockCredentialBasicAuthSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockCredentialBasicAuthSDK { + mock := &MockCredentialBasicAuthSDK{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index d96d27b8e..9f616e7bc 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -63,6 +63,11 @@ func (w sdkWrapper) GetMeSDK() MeSDK { return w.sdk.Me } +// GetBasicAuthCredentials returns the BasicAuthCredentials SDK to get current organization. +func (w sdkWrapper) GetBasicAuthCredentials() CredentialBasicAuthSDK { + return w.sdk.BasicAuthCredentials +} + // SDKToken is a token used to authenticate with the Konnect SDK. type SDKToken string diff --git a/controller/konnect/reconciler_credential_secrets.go b/controller/konnect/reconciler_credential_secrets.go new file mode 100644 index 000000000..80a112bd0 --- /dev/null +++ b/controller/konnect/reconciler_credential_secrets.go @@ -0,0 +1,283 @@ +package konnect + +import ( + "context" + "fmt" + "reflect" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/kong/gateway-operator/controller/pkg/log" + operatorerrors "github.com/kong/gateway-operator/internal/errors" + "github.com/kong/gateway-operator/modules/manager/scheme" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +const ( + // KongCredentialTypeBasicAuth is the type of basic-auth credential. + KongCredentialTypeBasicAuth = "basic-auth" +) + +// KongCredentialSecretReconciler reconciles a KongPlugin object. +type KongCredentialSecretReconciler struct { + developmentMode bool + client client.Client +} + +// NewKongCredentialSecretReconciler creates a new KongCredentialSecretReconciler. +func NewKongCredentialSecretReconciler( + developmentMode bool, + client client.Client, +) *KongCredentialSecretReconciler { + return &KongCredentialSecretReconciler{ + developmentMode: developmentMode, + client: client, + } +} + +// SetupWithManager sets up the controller with the Manager. +func (r *KongCredentialSecretReconciler) SetupWithManager(_ context.Context, mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + Named("KongCredentialSecret"). + For( + &corev1.Secret{}, + builder.WithPredicates( + predicate.NewPredicateFuncs( + secretIsUsedByConsumerAttachedToKonnectControlPlane(mgr.GetClient()), + ), + ), + ). + Watches(&configurationv1.KongConsumer{}, + handler.EnqueueRequestsFromMapFunc(enqueueSecretForKongConsumer), + builder.WithPredicates( + predicate.NewPredicateFuncs( + kongConsumerRefersToKonnectGatewayControlPlane, + ), + ), + ). + Owns(&configurationv1alpha1.CredentialBasicAuth{}). + // TODO: add more credentials types support. + // https://github.com/Kong/gateway-operator/issues/619 + // https://github.com/Kong/gateway-operator/issues/620 + // https://github.com/Kong/gateway-operator/issues/621 + // https://github.com/Kong/gateway-operator/issues/622 + Complete(r) +} + +func enqueueSecretForKongConsumer(ctx context.Context, obj client.Object) []reconcile.Request { + consumer, ok := obj.(*configurationv1.KongConsumer) + if !ok { + return nil + } + + var ret []ctrl.Request + for _, secretName := range consumer.Credentials { + ret = append(ret, ctrl.Request{ + NamespacedName: types.NamespacedName{ + Name: secretName, + Namespace: consumer.Namespace, + }, + }) + } + return ret +} + +// secretIsUsedByConsumerAttachedToKonnectControlPlane returns true if the Secret +// is used by KongConsumer which refers to a KonnectGatewayControlPlane. +func secretIsUsedByConsumerAttachedToKonnectControlPlane(cl client.Client) func(obj client.Object) bool { + return func(obj client.Object) bool { + secret, ok := obj.(*corev1.Secret) + if !ok { + ctrllog.FromContext(context.Background()).Error( + operatorerrors.ErrUnexpectedObject, + "failed to run predicate function", + "expected", "Secret", "found", reflect.TypeOf(obj), + ) + return false + } + + // List consumers using this Secret as credential. + kongConsumerList := configurationv1.KongConsumerList{} + err := cl.List( + context.Background(), + &kongConsumerList, + client.MatchingFields{ + IndexFieldKongConsumerReferencesSecrets: secret.GetName(), + }, + ) + if err != nil { + return false + } + + for _, kongConsumer := range kongConsumerList.Items { + cpRef := kongConsumer.Spec.ControlPlaneRef + if cpRef != nil && cpRef.Type == configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef { + return true + } + } + return false + } +} + +// Reconcile reconciles a Secrets that are used as Consumers credentials. +func (r *KongCredentialSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var ( + entityTypeName = "Secret" + logger = log.GetLogger(ctx, entityTypeName, r.developmentMode) + ) + + var secret corev1.Secret + if err := r.client.Get(ctx, req.NamespacedName, &secret); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + log.Debug(logger, "reconciling", secret) + cl := client.NewNamespacedClient(r.client, secret.Namespace) + + credType, err := extractKongCredentialType(&secret) + if err != nil { + return ctrl.Result{}, err + } + + // List consumers using this secret as credential. + kongConsumerList := configurationv1.KongConsumerList{} + err = cl.List( + ctx, + &kongConsumerList, + client.MatchingFields{ + IndexFieldKongConsumerReferencesSecrets: secret.GetName(), + }, + ) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed listing KongConsumers for Secret: %w", err) + } + + switch len(kongConsumerList.Items) { + case 0: + // If there are no Consumers that use the Secret then remove all the Credentials that use it. + + // TODO: add more credentials types support. + // https://github.com/Kong/gateway-operator/issues/619 + // https://github.com/Kong/gateway-operator/issues/620 + // https://github.com/Kong/gateway-operator/issues/621 + // https://github.com/Kong/gateway-operator/issues/622 + list := configurationv1alpha1.CredentialBasicAuthList{} + err := cl.List(ctx, &list, + client.MatchingFields{ + IndexFieldCredentialReferencesKongSecret: secret.Name, + }, + ) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed deleting CredentialBasicAuths: %w", err) + } + for _, credential := range list.Items { + if err = cl.Delete(ctx, &credential); err != nil { + return ctrl.Result{}, err + } + } + + default: + for _, kongConsumer := range kongConsumerList.Items { + // TODO: add more credentials types support. + // https://github.com/Kong/gateway-operator/issues/619 + // https://github.com/Kong/gateway-operator/issues/620 + // https://github.com/Kong/gateway-operator/issues/621 + // https://github.com/Kong/gateway-operator/issues/622 + switch credType { //nolint:gocritic + case KongCredentialTypeBasicAuth: + kongCredentialsBasicAuthList := configurationv1alpha1.CredentialBasicAuthList{} + err := cl.List( + ctx, + &kongCredentialsBasicAuthList, + client.MatchingFields{ + IndexFieldCredentialReferencesKongConsumer: kongConsumer.Name, + IndexFieldCredentialReferencesKongSecret: secret.Name, + }, + ) + if err != nil { + return ctrl.Result{}, fmt.Errorf("failed listing CredentialBasicAuth: %w", err) + } + + switch len(kongCredentialsBasicAuthList.Items) { + case 0: + credentialBasicAuth := configurationv1alpha1.CredentialBasicAuth{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: kongConsumer.Name + "-", + Namespace: kongConsumer.Namespace, + }, + Spec: configurationv1alpha1.CredentialBasicAuthSpec{ + ConsumerRef: corev1.LocalObjectReference{ + Name: kongConsumer.Name, + }, + SecretRef: corev1.LocalObjectReference{ + Name: secret.Name, + }, + // TODO: fill in the config + }, + } + err := controllerutil.SetControllerReference(&kongConsumer, &credentialBasicAuth, scheme.Get()) + if err != nil { + return ctrl.Result{}, err + } + if err = cl.Create(ctx, &credentialBasicAuth); err != nil { + return ctrl.Result{}, err + } + + default: + credentialBasicAuth := kongCredentialsBasicAuthList.Items[0] + if !credentialBasicAuth.DeletionTimestamp.IsZero() { + continue + } + + err := controllerutil.SetControllerReference(&kongConsumer, &credentialBasicAuth, scheme.Get()) + if err != nil { + return ctrl.Result{}, err + } + if credentialBasicAuth.Spec.ConsumerRef.Name != kongConsumer.Name || + credentialBasicAuth.Spec.SecretRef.Name != secret.Name { + if err = cl.Update(ctx, &credentialBasicAuth); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{ + Requeue: true, + }, nil + } + return ctrl.Result{}, err + } + } + } + } + } + } + + log.Debug(logger, "reconciliation completed", secret) + return ctrl.Result{}, nil +} + +const ( + // CredentialTypeLabel is the label key for the credential type. + CredentialTypeLabel = "konghq.com/credential" //nolint:gosec +) + +// extractKongCredentialType returns the credential type of a Secret or an error if no credential type is present. +// TODO(pmalek): consider migrating this into kubernetes-configuration +func extractKongCredentialType(secret *corev1.Secret) (string, error) { + credType, ok := secret.Labels[CredentialTypeLabel] + if !ok { + return "", fmt.Errorf("Secret %s/%s used as credential, but lacks %s label", + secret.Namespace, secret.Name, CredentialTypeLabel) + } + return credType, nil +} diff --git a/controller/konnect/reconciler_credential_secrets_rbac.go b/controller/konnect/reconciler_credential_secrets_rbac.go new file mode 100644 index 000000000..ac84b8bcf --- /dev/null +++ b/controller/konnect/reconciler_credential_secrets_rbac.go @@ -0,0 +1,7 @@ +package konnect + +//+kubebuilder:rbac:groups=konnect.konghq.com,resources=kongconsumers,verbs=get;list;watch + +//+kubebuilder:rbac:groups=konnect.konghq.com,resources=credentialbasicauths,verbs=get;list;watch;create;update;patch;delete + +//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch diff --git a/controller/konnect/reconciler_generic_rbac.go b/controller/konnect/reconciler_generic_rbac.go index 3756fe269..9d2b2a814 100644 --- a/controller/konnect/reconciler_generic_rbac.go +++ b/controller/konnect/reconciler_generic_rbac.go @@ -18,4 +18,10 @@ package konnect //+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongupstreams,verbs=get;list;watch;update;patch //+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongupstreams/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongconsumers,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongconsumers/status,verbs=get;update;patch + +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongconsumergroups,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongconsumergroups/status,verbs=get;update;patch + //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch diff --git a/go.mod b/go.mod index 47f843640..d1437d748 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/go-logr/logr v1.4.2 github.com/google/go-containerregistry v0.20.2 github.com/google/uuid v1.6.0 - github.com/kong/kubernetes-configuration v0.0.14 + github.com/kong/kubernetes-configuration v0.0.15-0.20240919082540-090267a78b9f github.com/kong/kubernetes-telemetry v0.1.5 github.com/kong/kubernetes-testing-framework v0.47.2 github.com/kong/semver/v4 v4.0.1 diff --git a/go.sum b/go.sum index 9bf0fd23f..24b339fbb 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,8 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kong/go-kong v0.59.1 h1:AJZtyCD+Zyqe/mF/m+x3/qN/GPVxAH7jq9zGJTHRfjc= github.com/kong/go-kong v0.59.1/go.mod h1:8Vt6HmtgLNgL/7bSwAlz3DIWqBtzG7qEt9+OnMiQOa0= -github.com/kong/kubernetes-configuration v0.0.14 h1:ukBhzqJWgArVF2FWtlnwnlEpMu9bT+fJWC3RI+hGvsI= -github.com/kong/kubernetes-configuration v0.0.14/go.mod h1:HA9tf7ftoGxLWrwSbFrs0ZRNk6fwrysNCih0PwgM1Zg= +github.com/kong/kubernetes-configuration v0.0.15-0.20240919082540-090267a78b9f h1:DiERmRXv8pVB7wkNaRN2k48pdmWfRh49WIIKDDrJAj4= +github.com/kong/kubernetes-configuration v0.0.15-0.20240919082540-090267a78b9f/go.mod h1:HA9tf7ftoGxLWrwSbFrs0ZRNk6fwrysNCih0PwgM1Zg= github.com/kong/kubernetes-telemetry v0.1.5 h1:xHwU1q0IvfEYqpj03po73ZKbVarnFPUwzkoFkdVnr9w= github.com/kong/kubernetes-telemetry v0.1.5/go.mod h1:1UXyZ6N3e8Fl6YguToQ6tKNveonkhjSqxzY7HVW+Ba4= github.com/kong/kubernetes-testing-framework v0.47.2 h1:+2Z9anTpbV/hwNeN+NFQz53BMU+g3QJydkweBp3tULo= diff --git a/modules/manager/controller_setup.go b/modules/manager/controller_setup.go index e5a07b400..995057cf4 100644 --- a/modules/manager/controller_setup.go +++ b/modules/manager/controller_setup.go @@ -80,6 +80,8 @@ const ( KongUpstreamControllerName = "KongUpstream" // KongServicePluginBindingFinalizerControllerName is the name of the KongService PluginBinding finalizer controller. KongServicePluginBindingFinalizerControllerName = "KongServicePluginBindingFinalizer" + // KongCredentialsSecretControllerName is the name of the Credentials Secret controller. + KongCredentialsSecretControllerName = "KongCredentialSecret" ) // SetupControllersShim runs SetupControllers and returns its result as a slice of the map values. @@ -410,6 +412,14 @@ func SetupControllers(mgr manager.Manager, c *Config) (map[string]ControllerDef, mgr.GetClient(), ), }, + // TODO(pmalek) + KongCredentialsSecretControllerName: { + Enabled: c.KonnectControllersEnabled, + Controller: konnect.NewKongCredentialSecretReconciler( + c.DevelopmentMode, + mgr.GetClient(), + ), + }, } // Merge Konnect controllers into the controllers map. This is done this way instead of directly assigning @@ -433,6 +443,14 @@ func SetupCacheIndicesForKonnectTypes(ctx context.Context, mgr manager.Manager, return fmt.Errorf("failed to setup cache indices for %s: %w", constraints.EntityTypeName[configurationv1alpha1.KongPluginBinding](), err) } + if err := setupCacheIndicesForKonnectType[configurationv1.KongConsumer](ctx, mgr, developmentMode); err != nil { + return fmt.Errorf("failed to setup cache indices for %s: %w", + constraints.EntityTypeName[configurationv1.KongConsumer](), err) + } + if err := setupCacheIndicesForKonnectType[configurationv1alpha1.CredentialBasicAuth](ctx, mgr, developmentMode); err != nil { + return fmt.Errorf("failed to setup cache indices for %s: %w", + constraints.EntityTypeName[configurationv1alpha1.CredentialBasicAuth](), err) + } return nil }