From 97029447d466c041da61f0bacdc386eb3b20572b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Fri, 20 Sep 2024 13:46:28 +0200 Subject: [PATCH] feat(konnect): add support for basic auth credentials for consumers (#625) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(konnect): add support for basic auth credentials for consumers * Apply suggestions from code review Co-authored-by: Grzegorz Burzyński * chore: refactor getServiceRef * fix: watch for consumer changes in CredentialBasicAuth reconciler * fix: fix handling Consumer deletion for credentials * refactor(konnect): refactor ReferencedKongConsumerIsBeingDeleted handling * Update controller/konnect/errors.go Co-authored-by: Grzegorz Burzyński --------- Co-authored-by: Grzegorz Burzyński --- .mockery.yaml | 1 + CHANGELOG.md | 2 + config/rbac/role/role.yaml | 2 + .../konnect_kongconsumer_basicauth.yaml | 46 ++++ controller/konnect/conditions/conditions.go | 14 + controller/konnect/constraints/constraints.go | 1 + controller/konnect/errors.go | 26 ++ controller/konnect/index.go | 4 +- .../konnect/index_credentials_basicauth.go | 32 +++ controller/konnect/ops/credentialbasicauth.go | 14 + .../konnect/ops/credentialbasicauth_mock.go | 259 ++++++++++++++++++ controller/konnect/ops/ops.go | 6 + .../konnect/ops/ops_credentialbasicauth.go | 183 +++++++++++++ controller/konnect/ops/sdkfactory.go | 6 + controller/konnect/ops/sdkfactory_mock.go | 38 +-- controller/konnect/reconciler_generic.go | 236 +++++++++++++++- controller/konnect/reconciler_generic_rbac.go | 9 + controller/konnect/watch.go | 2 + .../konnect/watch_credentialbasicauth.go | 250 +++++++++++++++++ go.mod | 2 +- go.sum | 4 +- modules/manager/controller_setup.go | 19 +- test/envtest/deploy_resources.go | 61 +++++ .../kongconsumercredential_basicauth_test.go | 165 +++++++++++ test/envtest/konnect_entities_suite_test.go | 1 + 25 files changed, 1353 insertions(+), 30 deletions(-) create mode 100644 config/samples/konnect_kongconsumer_basicauth.yaml create mode 100644 controller/konnect/index_credentials_basicauth.go create mode 100644 controller/konnect/ops/credentialbasicauth.go create mode 100644 controller/konnect/ops/credentialbasicauth_mock.go create mode 100644 controller/konnect/ops/ops_credentialbasicauth.go create mode 100644 controller/konnect/watch_credentialbasicauth.go create mode 100644 test/envtest/kongconsumercredential_basicauth_test.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/CHANGELOG.md b/CHANGELOG.md index ad8d82a8a..538ea71b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,8 @@ the creation of a managed `KongPluginBinding` resource, which is taken by the `KongPluginBinding` reconciler to create the corresponding plugin object in Konnect. [#550](https://github.com/Kong/gateway-operator/pull/550) +- Add support for `KongConsumer` credentials: + - basic-auth [#625](https://github.com/Kong/gateway-operator/pull/625) ### Fixed diff --git a/config/rbac/role/role.yaml b/config/rbac/role/role.yaml index 4a608fc44..afa4f744f 100644 --- a/config/rbac/role/role.yaml +++ b/config/rbac/role/role.yaml @@ -125,6 +125,7 @@ rules: - apiGroups: - configuration.konghq.com resources: + - credentialbasicauths - ingressclassparameterses - kongclusterplugins - kongconsumergroups @@ -143,6 +144,7 @@ rules: - apiGroups: - configuration.konghq.com resources: + - credentialbasicauths/status - kongclusterplugins/status - kongconsumergroups/status - kongconsumers/status diff --git a/config/samples/konnect_kongconsumer_basicauth.yaml b/config/samples/konnect_kongconsumer_basicauth.yaml new file mode 100644 index 000000000..4e8f14c58 --- /dev/null +++ b/config/samples/konnect_kongconsumer_basicauth.yaml @@ -0,0 +1,46 @@ +kind: KonnectAPIAuthConfiguration +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: konnect-api-auth-dev-1 + namespace: default +spec: + type: token + token: kpat_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + serverURL: us.api.konghq.com +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: test-cp-basic-auth + namespace: default +spec: + name: test-cp-basic-auth + labels: + app: test-cp-basic-auth + key1: test-cp-basic-auth + konnect: + authRef: + name: konnect-api-auth-dev-1 +--- +kind: KongConsumer +apiVersion: configuration.konghq.com/v1 +metadata: + name: consumer1 + namespace: default +username: consumer1 +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: test-cp-basic-auth +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: CredentialBasicAuth +metadata: + name: basic-auth-1 + namespace: default +spec: + consumerRef: + name: consumer1 + password: pass + username: username diff --git a/controller/konnect/conditions/conditions.go b/controller/konnect/conditions/conditions.go index bce9d594c..7f37b2f45 100644 --- a/controller/konnect/conditions/conditions.go +++ b/controller/konnect/conditions/conditions.go @@ -80,3 +80,17 @@ const ( // condition type indicating that the KongService reference is invalid. KongServiceRefReasonInvalid = "Invalid" ) + +const ( + // KongConsumerRefValidConditionType is the type of the condition that indicates + // whether the KongConsumer reference is valid and points to an existing + // KongConsumer. + KongConsumerRefValidConditionType = "KongConsumerRefValid" + + // KongConsumerRefReasonValid is the reason used with the KongConsumerRefValid + // condition type indicating that the KongConsumer reference is valid. + KongConsumerRefReasonValid = "Valid" + // KongConsumerRefReasonInvalid is the reason used with the KongConsumerRefValid + // condition type indicating that the KongConsumer reference is invalid. + KongConsumerRefReasonInvalid = "Invalid" +) 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/errors.go b/controller/konnect/errors.go index 07c11aa2c..e38d1dba6 100644 --- a/controller/konnect/errors.go +++ b/controller/konnect/errors.go @@ -2,6 +2,7 @@ package konnect import ( "fmt" + "time" "k8s.io/apimachinery/pkg/types" ) @@ -35,3 +36,28 @@ type ReferencedKongServiceIsBeingDeleted struct { func (e ReferencedKongServiceIsBeingDeleted) Error() string { return fmt.Sprintf("referenced Kong Service %s is being deleted", e.Reference) } + +// ReferencedKongConsumerIsBeingDeleted is an error type that is returned when +// a Konnect entity references a Kong Consumer which is being deleted. +type ReferencedKongConsumerIsBeingDeleted struct { + Reference types.NamespacedName + DeletionTimestamp time.Time +} + +// Error implements the error interface. +func (e ReferencedKongConsumerIsBeingDeleted) Error() string { + return fmt.Sprintf("referenced Kong Consumer %s is being deleted (deletion timestamp: %s)", + e.Reference, e.DeletionTimestamp, + ) +} + +// ReferencedKongConsumerDoesNotExist is an error type that is returned when the referenced KongConsumer does not exist. +type ReferencedKongConsumerDoesNotExist struct { + Reference types.NamespacedName + Err error +} + +// Error implements the error interface. +func (e ReferencedKongConsumerDoesNotExist) Error() string { + return fmt.Sprintf("referenced Kong Consumer %s does not exist: %v", e.Reference, e.Err) +} diff --git a/controller/konnect/index.go b/controller/konnect/index.go index 9719a65ba..e0d9fc65e 100644 --- a/controller/konnect/index.go +++ b/controller/konnect/index.go @@ -21,9 +21,11 @@ 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 *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..6f7e5d3b0 --- /dev/null +++ b/controller/konnect/index_credentials_basicauth.go @@ -0,0 +1,32 @@ +package konnect + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +const ( + // IndexFieldCredentialBasicAuthReferencesKongConsumer is the index name for CredentialBasicAuth -> Consumer. + IndexFieldCredentialBasicAuthReferencesKongConsumer = "kongCredentialsBasicAuthConsumerRef" +) + +// IndexOptionsForCredentialsBasicAuth returns required Index options for CredentialBasicAuth. +func IndexOptionsForCredentialsBasicAuth() []ReconciliationIndexOption { + return []ReconciliationIndexOption{ + { + IndexObject: &configurationv1alpha1.CredentialBasicAuth{}, + IndexField: IndexFieldCredentialBasicAuthReferencesKongConsumer, + ExtractValue: kongCredentialBasicAuthReferencesConsumer, + }, + } +} + +// 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} +} diff --git a/controller/konnect/ops/credentialbasicauth.go b/controller/konnect/ops/credentialbasicauth.go new file mode 100644 index 000000000..9c94239c9 --- /dev/null +++ b/controller/konnect/ops/credentialbasicauth.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/credentialbasicauth_mock.go b/controller/konnect/ops/credentialbasicauth_mock.go new file mode 100644 index 000000000..cd8ebc737 --- /dev/null +++ b/controller/konnect/ops/credentialbasicauth_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/ops.go b/controller/konnect/ops/ops.go index 46f9a88fc..52d2c2126 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -66,6 +66,8 @@ func Create[ return e, createPlugin(ctx, cl, sdk.GetPluginSDK(), ent) case *configurationv1alpha1.KongUpstream: return e, createUpstream(ctx, sdk.GetUpstreamsSDK(), ent) + case *configurationv1alpha1.CredentialBasicAuth: + return e, createCredentialBasicAuth(ctx, sdk.GetBasicAuthCredentials(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types @@ -106,6 +108,8 @@ func Delete[ return deletePlugin(ctx, sdk.GetPluginSDK(), ent) case *configurationv1alpha1.KongUpstream: return deleteUpstream(ctx, sdk.GetUpstreamsSDK(), ent) + case *configurationv1alpha1.CredentialBasicAuth: + return deleteCredentialBasicAuth(ctx, sdk.GetBasicAuthCredentials(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types @@ -171,6 +175,8 @@ func Update[ return ctrl.Result{}, updatePlugin(ctx, sdk.GetPluginSDK(), cl, ent) case *configurationv1alpha1.KongUpstream: return ctrl.Result{}, updateUpstream(ctx, sdk.GetUpstreamsSDK(), ent) + case *configurationv1alpha1.CredentialBasicAuth: + return ctrl.Result{}, updateCredentialBasicAuth(ctx, sdk.GetBasicAuthCredentials(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types diff --git a/controller/konnect/ops/ops_credentialbasicauth.go b/controller/konnect/ops/ops_credentialbasicauth.go new file mode 100644 index 000000000..9f16cb495 --- /dev/null +++ b/controller/konnect/ops/ops_credentialbasicauth.go @@ -0,0 +1,183 @@ +package ops + +import ( + "context" + "errors" + "fmt" + "slices" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" + "github.com/samber/lo" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/kong/gateway-operator/controller/konnect/conditions" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + "github.com/kong/kubernetes-configuration/pkg/metadata" +) + +func createCredentialBasicAuth( + ctx context.Context, + sdk CredentialBasicAuthSDK, + cred *configurationv1alpha1.CredentialBasicAuth, +) error { + cpID := cred.GetControlPlaneID() + if cpID == "" { + return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", cred, client.ObjectKeyFromObject(cred)) + } + + resp, err := sdk.CreateBasicAuthWithConsumer(ctx, + sdkkonnectops.CreateBasicAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + BasicAuthWithoutParents: credentialBasicAuthToBasicAuthWithoutParents(cred), + }, + ) + + // TODO: handle already exists + // Can't adopt it as it will cause conflicts between the controller + // that created that entity and already manages it, hm + if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, cred); errWrapped != nil { + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionFalse, + "FailedToCreate", + errWrapped.Error(), + cred.GetGeneration(), + ), + cred, + ) + return errWrapped + } + + cred.Status.Konnect.SetKonnectID(*resp.BasicAuth.ID) + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionTrue, + conditions.KonnectEntityProgrammedReasonProgrammed, + "", + cred.GetGeneration(), + ), + cred, + ) + + return nil +} + +// updateCredentialBasicAuth updates the Konnect BasicAuth entity. +// It is assumed that the provided BasicAuth has Konnect ID set in status. +// It returns an error if the BasicAuth does not have a ControlPlaneRef or +// if the operation fails. +func updateCredentialBasicAuth( + ctx context.Context, + sdk CredentialBasicAuthSDK, + cred *configurationv1alpha1.CredentialBasicAuth, +) error { + cpID := cred.GetControlPlaneID() + if cpID == "" { + return fmt.Errorf("can't update %T %s without a Konnect ControlPlane ID", cred, client.ObjectKeyFromObject(cred)) + } + + _, err := sdk.UpsertBasicAuthWithConsumer(ctx, sdkkonnectops.UpsertBasicAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + BasicAuthID: cred.GetKonnectStatus().GetKonnectID(), + BasicAuthWithoutParents: credentialBasicAuthToBasicAuthWithoutParents(cred), + }) + + // TODO: handle already exists + // Can't adopt it as it will cause conflicts between the controller + // that created that entity and already manages it, hm + if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, cred); errWrapped != nil { + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionFalse, + "FailedToCreate", + errWrapped.Error(), + cred.GetGeneration(), + ), + cred, + ) + return errWrapped + } + + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionTrue, + conditions.KonnectEntityProgrammedReasonProgrammed, + "", + cred.GetGeneration(), + ), + cred, + ) + + return nil +} + +// deleteCredentialBasicAuth deletes a BasicAuth credential in Konnect. +// It is assumed that the provided BasicAuth has Konnect ID set in status. +// It returns an error if the operation fails. +func deleteCredentialBasicAuth( + ctx context.Context, + sdk CredentialBasicAuthSDK, + cred *configurationv1alpha1.CredentialBasicAuth, +) error { + cpID := cred.GetControlPlaneID() + id := cred.GetKonnectStatus().GetKonnectID() + _, err := sdk.DeleteBasicAuthWithConsumer(ctx, + sdkkonnectops.DeleteBasicAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + BasicAuthID: id, + }) + if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, cred); errWrapped != nil { + // Service delete operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrapped, &sdkError) { + if sdkError.StatusCode == 404 { + ctrllog.FromContext(ctx). + Info("entity not found in Konnect, skipping delete", + "op", DeleteOp, "type", cred.GetTypeName(), "id", id, + ) + return nil + } + return FailedKonnectOpError[configurationv1alpha1.CredentialBasicAuth]{ + Op: DeleteOp, + Err: sdkError, + } + } + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: DeleteOp, + Err: errWrapped, + } + } + + return nil +} + +func credentialBasicAuthToBasicAuthWithoutParents( + cred *configurationv1alpha1.CredentialBasicAuth, +) sdkkonnectcomp.BasicAuthWithoutParents { + var ( + specTags = cred.Spec.Tags + annotationTags = metadata.ExtractTags(cred) + k8sTags = GenerateKubernetesMetadataTags(cred) + ) + // Deduplicate tags to avoid rejection by Konnect. + tags := lo.Uniq(slices.Concat(specTags, annotationTags, k8sTags)) + + return sdkkonnectcomp.BasicAuthWithoutParents{ + Password: lo.ToPtr(cred.Spec.Password), + Username: lo.ToPtr(cred.Spec.Username), + Tags: tags, + } +} diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index d96d27b8e..c15b7e306 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -15,6 +15,7 @@ type SDKWrapper interface { GetPluginSDK() PluginSDK GetUpstreamsSDK() UpstreamsSDK GetMeSDK() MeSDK + GetBasicAuthCredentials() CredentialBasicAuthSDK } type sdkWrapper struct { @@ -63,6 +64,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/ops/sdkfactory_mock.go b/controller/konnect/ops/sdkfactory_mock.go index a035900ae..54f83f0e3 100644 --- a/controller/konnect/ops/sdkfactory_mock.go +++ b/controller/konnect/ops/sdkfactory_mock.go @@ -7,28 +7,30 @@ import ( ) type MockSDKWrapper struct { - ControlPlaneSDK *MockControlPlaneSDK - ServicesSDK *MockServicesSDK - RoutesSDK *MockRoutesSDK - ConsumersSDK *MockConsumersSDK - ConsumerGroupSDK *MockConsumerGroupSDK - PluginSDK *MockPluginSDK - UpstreamsSDK *MockUpstreamsSDK - MeSDK *MockMeSDK + ControlPlaneSDK *MockControlPlaneSDK + ServicesSDK *MockServicesSDK + RoutesSDK *MockRoutesSDK + ConsumersSDK *MockConsumersSDK + ConsumerGroupSDK *MockConsumerGroupSDK + PluginSDK *MockPluginSDK + UpstreamsSDK *MockUpstreamsSDK + MeSDK *MockMeSDK + BasicAuthCredentials *MockCredentialBasicAuthSDK } var _ SDKWrapper = MockSDKWrapper{} func NewMockSDKWrapperWithT(t *testing.T) *MockSDKWrapper { return &MockSDKWrapper{ - ControlPlaneSDK: NewMockControlPlaneSDK(t), - ServicesSDK: NewMockServicesSDK(t), - RoutesSDK: NewMockRoutesSDK(t), - ConsumersSDK: NewMockConsumersSDK(t), - ConsumerGroupSDK: NewMockConsumerGroupSDK(t), - PluginSDK: NewMockPluginSDK(t), - UpstreamsSDK: NewMockUpstreamsSDK(t), - MeSDK: NewMockMeSDK(t), + ControlPlaneSDK: NewMockControlPlaneSDK(t), + ServicesSDK: NewMockServicesSDK(t), + RoutesSDK: NewMockRoutesSDK(t), + ConsumersSDK: NewMockConsumersSDK(t), + ConsumerGroupSDK: NewMockConsumerGroupSDK(t), + PluginSDK: NewMockPluginSDK(t), + UpstreamsSDK: NewMockUpstreamsSDK(t), + MeSDK: NewMockMeSDK(t), + BasicAuthCredentials: NewMockCredentialBasicAuthSDK(t), } } @@ -60,6 +62,10 @@ func (m MockSDKWrapper) GetUpstreamsSDK() UpstreamsSDK { return m.UpstreamsSDK } +func (m MockSDKWrapper) GetBasicAuthCredentials() CredentialBasicAuthSDK { + return m.BasicAuthCredentials +} + func (m MockSDKWrapper) GetMeSDK() MeSDK { return m.MeSDK } diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index 82d8282c2..592c444e8 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -7,6 +7,7 @@ import ( "time" "github.com/samber/mo" + 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" @@ -169,6 +170,46 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } else if res.Requeue { return res, nil } + // If a type has a KongConsumer ref, handle it. + res, err = handleKongConsumerRef(ctx, r.Client, ent) + if err != nil { + // If the referenced KongConsumer is being deleted and the object + // is not being deleted yet then requeue until it will + // get the deletion timestamp set due to having the owner set to KongConsumer. + if errDel := (&ReferencedKongConsumerIsBeingDeleted{}); errors.As(err, errDel) && + ent.GetDeletionTimestamp().IsZero() { + return ctrl.Result{ + RequeueAfter: time.Until(errDel.DeletionTimestamp), + }, nil + } + + // If the referenced KongConsumer is not found or is being deleted + // and the object is being deleted, remove the finalizer and let the + // deletion proceed without trying to delete the entity from Konnect + // as the KongConsumer deletion will (or already has - in case of the consumer being gone) + // take care of it on the Konnect side. + if errors.As(err, &ReferencedKongConsumerDoesNotExist{}) || + errors.As(err, &ReferencedKongConsumerIsBeingDeleted{}) { + if !ent.GetDeletionTimestamp().IsZero() { + if controllerutil.RemoveFinalizer(ent, KonnectCleanupFinalizer) { + if err := r.Client.Update(ctx, ent); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to remove finalizer %s: %w", KonnectCleanupFinalizer, err) + } + log.Debug(logger, "finalizer removed as the owning KongConsumer is being deleted or is already gone", ent, + "finalizer", KonnectCleanupFinalizer, + ) + } + } + return ctrl.Result{}, nil + } + + return ctrl.Result{}, err + } else if res.Requeue { + return res, nil + } apiAuthRef, err := getAPIAuthRefNN(ctx, r.Client, ent) if err != nil { @@ -511,6 +552,28 @@ func getAPIAuthRefNN[T constraints.SupportedKonnectEntityType, TEnt constraints. return getCPAuthRefForRef(ctx, cl, cpRef, ent.GetNamespace()) } + // If the entity has a KongConsumerRef, get the KonnectAPIAuthConfiguration + // ref from the referenced KongConsumer. + consumerRef, ok := getConsumerRef(ent).Get() + if ok { + // TODO(pmalek): handle cross namespace refs + nn := types.NamespacedName{ + Name: consumerRef.Name, + Namespace: ent.GetNamespace(), + } + + var consumer configurationv1.KongConsumer + if err := cl.Get(ctx, nn, &consumer); err != nil { + return types.NamespacedName{}, fmt.Errorf("failed to get KongConsumer %s", nn) + } + + cpRef, ok := getControlPlaneRef(&consumer).Get() + if !ok { + return types.NamespacedName{}, fmt.Errorf("KongConsumer %s does not have a ControlPlaneRef", nn) + } + return getCPAuthRefForRef(ctx, cl, cpRef, ent.GetNamespace()) + } + if ref, ok := any(ent).(constraints.EntityWithKonnectAPIAuthConfigurationRef); ok { return types.NamespacedName{ Name: ref.GetKonnectAPIAuthConfigurationRef().Name, @@ -525,24 +588,28 @@ func getAPIAuthRefNN[T constraints.SupportedKonnectEntityType, TEnt constraints. ) } +func getConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( + e TEnt, +) mo.Option[corev1.LocalObjectReference] { + switch e := any(e).(type) { + case *configurationv1alpha1.CredentialBasicAuth: + return mo.Some(e.Spec.ConsumerRef) + default: + return mo.None[corev1.LocalObjectReference]() + } +} + func getServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( e TEnt, ) mo.Option[configurationv1alpha1.ServiceRef] { switch e := any(e).(type) { - case *configurationv1alpha1.KongService, - *configurationv1.KongConsumer, - *configurationv1beta1.KongConsumerGroup, - *konnectv1alpha1.KonnectGatewayControlPlane, - *configurationv1alpha1.KongPluginBinding, - *configurationv1alpha1.KongUpstream: - return mo.None[configurationv1alpha1.ServiceRef]() case *configurationv1alpha1.KongRoute: if e.Spec.ServiceRef == nil { return mo.None[configurationv1alpha1.ServiceRef]() } return mo.Some(*e.Spec.ServiceRef) default: - panic(fmt.Sprintf("unsupported entity type %T", e)) + return mo.None[configurationv1alpha1.ServiceRef]() } } @@ -701,11 +768,162 @@ func handleKongServiceRef[T constraints.SupportedKonnectEntityType, TEnt constra return ctrl.Result{}, nil } +// handleKongConsumerRef handles the ConsumerRef for the given entity. +// It sets the owner reference to the referenced KongConsumer and updates the +// status of the entity based on the referenced KongConsumer status. +func handleKongConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( + ctx context.Context, + cl client.Client, + ent TEnt, +) (ctrl.Result, error) { + kongConsumerRef, ok := getConsumerRef(ent).Get() + if !ok { + return ctrl.Result{}, nil + } + consumer := configurationv1.KongConsumer{} + nn := types.NamespacedName{ + Name: kongConsumerRef.Name, + Namespace: ent.GetNamespace(), + } + + if err := cl.Get(ctx, nn, &consumer); err != nil { + if res, errStatus := updateStatusWithCondition( + ctx, cl, ent, + conditions.KongConsumerRefValidConditionType, + metav1.ConditionFalse, + conditions.KongConsumerRefReasonInvalid, + err.Error(), + ); errStatus != nil || res.Requeue { + return res, errStatus + } + + return ctrl.Result{}, ReferencedKongConsumerDoesNotExist{ + Reference: nn, + Err: err, + } + } + + // If referenced KongConsumer is being deleted, return an error so that we + // can remove the entity from Konnect first. + if delTimestamp := consumer.GetDeletionTimestamp(); !delTimestamp.IsZero() { + return ctrl.Result{}, ReferencedKongConsumerIsBeingDeleted{ + Reference: nn, + DeletionTimestamp: delTimestamp.Time, + } + } + + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, &consumer) + if !ok || cond.Status != metav1.ConditionTrue { + ent.SetKonnectID("") + if res, err := updateStatusWithCondition( + ctx, cl, ent, + conditions.KongConsumerRefValidConditionType, + metav1.ConditionFalse, + conditions.KongConsumerRefReasonInvalid, + fmt.Sprintf("Referenced KongConsumer %s is not programmed yet", nn), + ); err != nil || res.Requeue { + return ctrl.Result{}, err + } + return ctrl.Result{Requeue: true}, nil + } + + old := ent.DeepCopyObject().(TEnt) + if err := controllerutil.SetOwnerReference(&consumer, ent, cl.Scheme(), controllerutil.WithBlockOwnerDeletion(true)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to set owner reference: %w", err) + } + if err := cl.Patch(ctx, ent, client.MergeFrom(old)); err != nil { + if k8serrors.IsConflict(err) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update status: %w", err) + } + + // TODO(pmalek): make this generic. + // Consumer ID is not stored in KonnectEntityStatus because not all entities + // have a ConsumerRef, hence the type constraints in the reconciler can't be used. + if cred, ok := any(ent).(*configurationv1alpha1.CredentialBasicAuth); ok { + if cred.Status.Konnect == nil { + cred.Status.Konnect = &konnectv1alpha1.KonnectEntityStatusWithControlPlaneAndConsumerRefs{} + } + cred.Status.Konnect.ConsumerID = consumer.Status.Konnect.GetKonnectID() + } + + if res, errStatus := updateStatusWithCondition( + ctx, cl, ent, + conditions.KongConsumerRefValidConditionType, + metav1.ConditionTrue, + conditions.KongConsumerRefReasonValid, + fmt.Sprintf("Referenced KongConsumer %s programmed", nn), + ); errStatus != nil || res.Requeue { + return res, errStatus + } + + cpRef, ok := getControlPlaneRef(&consumer).Get() + if !ok { + return ctrl.Result{}, fmt.Errorf( + "KongRoute references a KongConsumer %s which does not have a ControlPlane ref", + client.ObjectKeyFromObject(&consumer), + ) + } + cp, err := getCPForRef(ctx, cl, cpRef, ent.GetNamespace()) + if err != nil { + if res, errStatus := updateStatusWithCondition( + ctx, cl, ent, + conditions.ControlPlaneRefValidConditionType, + metav1.ConditionFalse, + conditions.ControlPlaneRefReasonInvalid, + err.Error(), + ); errStatus != nil || res.Requeue { + return res, errStatus + } + if k8serrors.IsNotFound(err) { + return ctrl.Result{}, ReferencedControlPlaneDoesNotExistError{ + Reference: nn, + Err: err, + } + } + return ctrl.Result{}, err + } + + cond, ok = k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() { + if res, errStatus := updateStatusWithCondition( + ctx, cl, ent, + conditions.ControlPlaneRefValidConditionType, + metav1.ConditionFalse, + conditions.ControlPlaneRefReasonInvalid, + fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn), + ); errStatus != nil || res.Requeue { + return res, errStatus + } + + return ctrl.Result{Requeue: true}, nil + } + + if resource, ok := any(ent).(EntityWithControlPlaneRef); ok { + resource.SetControlPlaneID(cp.Status.ID) + } + + if res, errStatus := updateStatusWithCondition( + ctx, cl, ent, + conditions.ControlPlaneRefValidConditionType, + metav1.ConditionTrue, + conditions.ControlPlaneRefReasonValid, + fmt.Sprintf("Referenced ControlPlane %s is programmed", nn), + ); errStatus != nil || res.Requeue { + return res, errStatus + } + + return ctrl.Result{}, nil +} + func getControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( e TEnt, ) mo.Option[configurationv1alpha1.ControlPlaneRef] { switch e := any(e).(type) { - case *konnectv1alpha1.KonnectGatewayControlPlane, *configurationv1alpha1.KongRoute: + case *konnectv1alpha1.KonnectGatewayControlPlane, + *configurationv1alpha1.KongRoute, + *configurationv1alpha1.CredentialBasicAuth: return mo.None[configurationv1alpha1.ControlPlaneRef]() case *configurationv1.KongConsumer: if e.Spec.ControlPlaneRef == nil { diff --git a/controller/konnect/reconciler_generic_rbac.go b/controller/konnect/reconciler_generic_rbac.go index 3756fe269..de73c7484 100644 --- a/controller/konnect/reconciler_generic_rbac.go +++ b/controller/konnect/reconciler_generic_rbac.go @@ -18,4 +18,13 @@ 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=configuration.konghq.com,resources=credentialbasicauths,verbs=get;list;watch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=credentialbasicauths/status,verbs=get;update;patch + //+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch diff --git a/controller/konnect/watch.go b/controller/konnect/watch.go index 5164db60d..02ab7d993 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -38,6 +38,8 @@ func ReconciliationWatchOptionsForEntity[ return KongPluginBindingReconciliationWatchOptions(cl) case *configurationv1alpha1.KongUpstream: return KongUpstreamReconciliationWatchOptions(cl) + case *configurationv1alpha1.CredentialBasicAuth: + return CredentialBasicAuthReconciliationWatchOptions(cl) default: panic(fmt.Sprintf("unsupported entity type %T", ent)) } diff --git a/controller/konnect/watch_credentialbasicauth.go b/controller/konnect/watch_credentialbasicauth.go new file mode 100644 index 000000000..89c997f63 --- /dev/null +++ b/controller/konnect/watch_credentialbasicauth.go @@ -0,0 +1,250 @@ +package konnect + +import ( + "context" + "reflect" + + "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/handler" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + operatorerrors "github.com/kong/gateway-operator/internal/errors" + + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +// TODO(pmalek): this can be extracted and used in reconciler.go +// as every Konnect entity will have a reference to the KonnectAPIAuthConfiguration. +// This would require: +// - mapping function from non List types to List types +// - a function on each Konnect entity type to get the API Auth configuration +// reference from the object +// - lists have their items stored in Items field, not returned via a method + +// CredentialBasicAuthReconciliationWatchOptions returns the watch options for +// the CredentialBasicAuth. +func CredentialBasicAuthReconciliationWatchOptions( + cl client.Client, +) []func(*ctrl.Builder) *ctrl.Builder { + return []func(*ctrl.Builder) *ctrl.Builder{ + func(b *ctrl.Builder) *ctrl.Builder { + return b.For(&configurationv1alpha1.CredentialBasicAuth{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(CredentialBasicAuthRefersToKonnectGatewayControlPlane(cl)), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &configurationv1.KongConsumer{}, + handler.EnqueueRequestsFromMapFunc( + credentialBasicAuthForKongConsumer(cl), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectAPIAuthConfiguration{}, + handler.EnqueueRequestsFromMapFunc( + credentialBasicAuthForKonnectAPIAuthConfiguration(cl), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectGatewayControlPlane{}, + handler.EnqueueRequestsFromMapFunc( + credentialBasicAuthForKonnectGatewayControlPlane(cl), + ), + ) + }, + } +} + +// CredentialBasicAuthRefersToKonnectGatewayControlPlane returns true if the CredentialBasicAuth +// refers to a KonnectGatewayControlPlane. +func CredentialBasicAuthRefersToKonnectGatewayControlPlane(cl client.Client) func(obj client.Object) bool { + return func(obj client.Object) bool { + credentialBasicAuth, ok := obj.(*configurationv1alpha1.CredentialBasicAuth) + if !ok { + ctrllog.FromContext(context.Background()).Error( + operatorerrors.ErrUnexpectedObject, + "failed to run predicate function", + "expected", "CredentialBasicAuth", "found", reflect.TypeOf(obj), + ) + return false + } + + consumerRef := credentialBasicAuth.Spec.ConsumerRef + nn := types.NamespacedName{ + Namespace: credentialBasicAuth.Namespace, + Name: consumerRef.Name, + } + consumer := configurationv1.KongConsumer{} + if err := cl.Get(context.Background(), nn, &consumer); client.IgnoreNotFound(err) != nil { + return true + } + + cpRef := consumer.Spec.ControlPlaneRef + return cpRef != nil && cpRef.Type == configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef + } +} + +func credentialBasicAuthForKonnectAPIAuthConfiguration( + cl client.Client, +) func(ctx context.Context, obj client.Object) []reconcile.Request { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + auth, ok := obj.(*konnectv1alpha1.KonnectAPIAuthConfiguration) + if !ok { + return nil + } + + var l configurationv1.KongConsumerList + if err := cl.List(ctx, &l, + // TODO: change this when cross namespace refs are allowed. + client.InNamespace(auth.GetNamespace()), + ); err != nil { + return nil + } + + var ret []reconcile.Request + for _, consumer := range l.Items { + cpRef := consumer.Spec.ControlPlaneRef + if cpRef == nil || + cpRef.Type != configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef || + cpRef.KonnectNamespacedRef == nil || + cpRef.KonnectNamespacedRef.Name != auth.GetName() { + continue + } + + cpNN := types.NamespacedName{ + Name: cpRef.KonnectNamespacedRef.Name, + Namespace: consumer.Namespace, + } + var cp konnectv1alpha1.KonnectGatewayControlPlane + if err := cl.Get(ctx, cpNN, &cp); err != nil { + ctrllog.FromContext(ctx).Error( + err, + "failed to get KonnectGatewayControlPlane", + "KonnectGatewayControlPlane", cpNN, + ) + continue + } + + // TODO: change this when cross namespace refs are allowed. + if cp.GetKonnectAPIAuthConfigurationRef().Name != auth.Name { + continue + } + + var credList configurationv1alpha1.CredentialBasicAuthList + if err := cl.List(ctx, &credList, + client.MatchingFields{ + IndexFieldCredentialBasicAuthReferencesKongConsumer: consumer.Name, + }, + client.InNamespace(auth.GetNamespace()), + ); err != nil { + return nil + } + + for _, cred := range credList.Items { + ret = append(ret, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: cred.Namespace, + Name: cred.Name, + }, + }, + ) + } + } + return ret + } +} + +func credentialBasicAuthForKonnectGatewayControlPlane( + cl client.Client, +) func(ctx context.Context, obj client.Object) []reconcile.Request { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + cp, ok := obj.(*konnectv1alpha1.KonnectGatewayControlPlane) + if !ok { + return nil + } + var l configurationv1.KongConsumerList + if err := cl.List(ctx, &l, + // TODO: change this when cross namespace refs are allowed. + client.InNamespace(cp.GetNamespace()), + ); err != nil { + return nil + } + + var ret []reconcile.Request + for _, consumer := range l.Items { + cpRef := consumer.Spec.ControlPlaneRef + if cpRef.Type != configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef || + cpRef.KonnectNamespacedRef == nil || + cpRef.KonnectNamespacedRef.Name != cp.GetName() { + continue + } + + var credList configurationv1alpha1.CredentialBasicAuthList + if err := cl.List(ctx, &credList, + client.MatchingFields{ + IndexFieldCredentialBasicAuthReferencesKongConsumer: consumer.Name, + }, + client.InNamespace(cp.GetNamespace()), + ); err != nil { + return nil + } + + for _, cred := range credList.Items { + ret = append(ret, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: cred.Namespace, + Name: cred.Name, + }, + }, + ) + } + } + return ret + } +} + +func credentialBasicAuthForKongConsumer( + cl client.Client, +) func(ctx context.Context, obj client.Object) []reconcile.Request { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + consumer, ok := obj.(*configurationv1.KongConsumer) + if !ok { + return nil + } + var l configurationv1alpha1.CredentialBasicAuthList + if err := cl.List(ctx, &l, + client.MatchingFields{ + IndexFieldCredentialBasicAuthReferencesKongConsumer: consumer.Name, + }, + // TODO: change this when cross namespace refs are allowed. + client.InNamespace(consumer.GetNamespace()), + ); err != nil { + return nil + } + + var ret []reconcile.Request + for _, cred := range l.Items { + ret = append(ret, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: cred.Namespace, + Name: cred.Name, + }, + }, + ) + } + return ret + } +} diff --git a/go.mod b/go.mod index 47f843640..426ddee85 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 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..588c2c467 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 h1:ijNJSF49W+KomBePL2bgBGkOPw+uML7lP0ZblIS0hIc= +github.com/kong/kubernetes-configuration v0.0.15/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..2a262a991 100644 --- a/modules/manager/controller_setup.go +++ b/modules/manager/controller_setup.go @@ -80,6 +80,10 @@ 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" + // KongCredentialBasicAuthControllerName is the name of the CredentialBasicAuth controller. + KongCredentialBasicAuthControllerName = "CredentialBasicAuth" //nolint:gosec ) // SetupControllersShim runs SetupControllers and returns its result as a slice of the map values. @@ -388,13 +392,22 @@ func SetupControllers(mgr manager.Manager, c *Config) (map[string]ControllerDef, }, KongPluginBindingControllerName: { Enabled: c.KonnectControllersEnabled, - Controller: konnect.NewKonnectEntityReconciler[configurationv1alpha1.KongPluginBinding]( + Controller: konnect.NewKonnectEntityReconciler( sdkFactory, c.DevelopmentMode, mgr.GetClient(), konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongPluginBinding](c.KonnectSyncPeriod), ), }, + KongCredentialBasicAuthControllerName: { + Enabled: c.KonnectControllersEnabled, + Controller: konnect.NewKonnectEntityReconciler( + sdkFactory, + c.DevelopmentMode, + mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.CredentialBasicAuth](c.KonnectSyncPeriod), + ), + }, KongPluginControllerName: { Enabled: c.KonnectControllersEnabled, Controller: konnect.NewKongPluginReconciler( @@ -433,6 +446,10 @@ 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[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 } diff --git a/test/envtest/deploy_resources.go b/test/envtest/deploy_resources.go index 40b99e11f..653ec2ac2 100644 --- a/test/envtest/deploy_resources.go +++ b/test/envtest/deploy_resources.go @@ -7,11 +7,13 @@ import ( "github.com/google/uuid" "github.com/samber/lo" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" "github.com/kong/gateway-operator/controller/konnect/conditions" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) @@ -142,6 +144,33 @@ func deployKongService( return kongService } +// deployKongConsumerWithProgrammed deploys a KongConsumer resource and returns the resource. +func deployKongConsumerWithProgrammed( + t *testing.T, + ctx context.Context, + cl client.Client, + consumer *configurationv1.KongConsumer, +) *configurationv1.KongConsumer { + t.Helper() + + consumer.GenerateName = "kongconsumer-" + require.NoError(t, cl.Create(ctx, consumer)) + t.Logf("deployed %s KongConsumer resource", client.ObjectKeyFromObject(consumer)) + + consumer.Status.Conditions = []metav1.Condition{ + { + Type: conditions.KonnectEntityProgrammedConditionType, + Status: metav1.ConditionTrue, + Reason: conditions.KonnectEntityProgrammedReasonProgrammed, + ObservedGeneration: consumer.GetGeneration(), + LastTransitionTime: metav1.Now(), + }, + } + require.NoError(t, cl.Status().Update(ctx, consumer)) + + return consumer +} + // deployKongPluginBinding deploys a KongPluginBinding resource and returns the resource. // The caller can also specify the status which will be updated on the resource. func deployKongPluginBinding( @@ -159,3 +188,35 @@ func deployKongPluginBinding( require.NoError(t, cl.Status().Update(ctx, kpb)) return kpb } + +// deployCredentialBasicAuth deploys a CredentialBasicAuth resource and returns the resource. +func deployCredentialBasicAuth( + t *testing.T, + ctx context.Context, + cl client.Client, + consumerName string, + username string, + password string, +) *configurationv1alpha1.CredentialBasicAuth { + t.Helper() + + c := &configurationv1alpha1.CredentialBasicAuth{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "basic-auth-", + }, + Spec: configurationv1alpha1.CredentialBasicAuthSpec{ + ConsumerRef: corev1.LocalObjectReference{ + Name: consumerName, + }, + CredentialBasicAuthAPISpec: configurationv1alpha1.CredentialBasicAuthAPISpec{ + Password: password, + Username: username, + }, + }, + } + + require.NoError(t, cl.Create(ctx, c)) + t.Logf("deployed new unmanaged CredentialBasicAuth %s", client.ObjectKeyFromObject(c)) + + return c +} diff --git a/test/envtest/kongconsumercredential_basicauth_test.go b/test/envtest/kongconsumercredential_basicauth_test.go new file mode 100644 index 000000000..c8b7dcf33 --- /dev/null +++ b/test/envtest/kongconsumercredential_basicauth_test.go @@ -0,0 +1,165 @@ +package envtest + +import ( + "context" + "testing" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + "github.com/google/uuid" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/watch" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kong/gateway-operator/controller/konnect" + "github.com/kong/gateway-operator/controller/konnect/ops" + "github.com/kong/gateway-operator/modules/manager" + "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" + "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func TestKongConsumerCredential_BasicAuth(t *testing.T) { + t.Parallel() + ctx, cancel := Context(t, context.Background()) + defer cancel() + + // Setup up the envtest environment. + cfg, ns := Setup(t, ctx, scheme.Get()) + + mgr, logs := NewManager(t, ctx, cfg, scheme.Get()) + + clientWithWatch, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: scheme.Get(), + }) + require.NoError(t, err) + clientNamespaced := client.NewNamespacedClient(mgr.GetClient(), ns.Name) + + apiAuth := deployKonnectAPIAuthConfigurationWithProgrammed(t, ctx, clientNamespaced) + cp := deployKonnectGatewayControlPlaneWithID(t, ctx, clientNamespaced, apiAuth) + + consumerID := uuid.NewString() + consumer := deployKongConsumerWithProgrammed(t, ctx, clientNamespaced, &configurationv1.KongConsumer{ + Username: "username1", + Spec: configurationv1.KongConsumerSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: cp.Name, + }, + }, + }, + }) + consumer.Status.Konnect = &v1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: cp.GetKonnectStatus().GetKonnectID(), + KonnectEntityStatus: v1alpha1.KonnectEntityStatus{ + ID: consumerID, + ServerURL: cp.GetKonnectStatus().GetServerURL(), + OrgID: cp.GetKonnectStatus().GetOrgID(), + }, + } + require.NoError(t, clientNamespaced.Status().Update(ctx, consumer)) + + password := "password" + username := "username" + credentialBasicAuth := deployCredentialBasicAuth(t, ctx, clientNamespaced, consumer.Name, username, password) + basicAuthID := uuid.NewString() + tags := []string{ + "k8s-generation:1", + "k8s-group:configuration.konghq.com", + "k8s-kind:CredentialBasicAuth", + "k8s-name:" + credentialBasicAuth.Name, + "k8s-uid:" + string(credentialBasicAuth.GetUID()), + "k8s-version:v1alpha1", + "k8s-namespace:" + ns.Name, + } + + factory := ops.NewMockSDKFactory(t) + factory.SDK.BasicAuthCredentials.EXPECT(). + CreateBasicAuthWithConsumer( + mock.Anything, + sdkkonnectops.CreateBasicAuthWithConsumerRequest{ + ControlPlaneID: cp.GetKonnectStatus().GetKonnectID(), + ConsumerIDForNestedEntities: consumerID, + BasicAuthWithoutParents: sdkkonnectcomp.BasicAuthWithoutParents{ + Password: lo.ToPtr(password), + Username: lo.ToPtr(username), + Tags: tags, + }, + }, + ). + Return( + &sdkkonnectops.CreateBasicAuthWithConsumerResponse{ + BasicAuth: &sdkkonnectcomp.BasicAuth{ + ID: lo.ToPtr(basicAuthID), + }, + }, + nil, + ) + factory.SDK.BasicAuthCredentials.EXPECT(). + UpsertBasicAuthWithConsumer(mock.Anything, mock.Anything, mock.Anything).Maybe(). + Return( + &sdkkonnectops.UpsertBasicAuthWithConsumerResponse{ + BasicAuth: &sdkkonnectcomp.BasicAuth{ + ID: lo.ToPtr(basicAuthID), + }, + }, + nil, + ) + + require.NoError(t, manager.SetupCacheIndicesForKonnectTypes(ctx, mgr, false)) + reconcilers := []Reconciler{ + konnect.NewKonnectEntityReconciler(factory, false, mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.CredentialBasicAuth](konnectSyncTime), + ), + } + + StartReconcilers(ctx, t, mgr, logs, reconcilers...) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.BasicAuthCredentials.AssertExpectations(t)) + }, waitTime, tickTime) + + factory.SDK.BasicAuthCredentials.EXPECT(). + DeleteBasicAuthWithConsumer( + mock.Anything, + sdkkonnectops.DeleteBasicAuthWithConsumerRequest{ + ControlPlaneID: cp.GetKonnectStatus().GetKonnectID(), + ConsumerIDForNestedEntities: consumerID, + BasicAuthID: basicAuthID, + }, + ). + Return( + &sdkkonnectops.DeleteBasicAuthWithConsumerResponse{ + StatusCode: 200, + }, + nil, + ) + require.NoError(t, clientNamespaced.Delete(ctx, credentialBasicAuth)) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.BasicAuthCredentials.AssertExpectations(t)) + }, waitTime, tickTime) + + w := setupWatch[configurationv1alpha1.CredentialBasicAuthList](t, ctx, clientWithWatch, client.InNamespace(ns.Name)) + + credentialBasicAuth = deployCredentialBasicAuth(t, ctx, clientNamespaced, consumer.Name, username, password) + t.Logf("redeployed %s CredentialBasicAuth resource", client.ObjectKeyFromObject(credentialBasicAuth)) + t.Logf("checking if KongConsumer %s removal will delete the associated credentials %s", + client.ObjectKeyFromObject(consumer), + client.ObjectKeyFromObject(credentialBasicAuth), + ) + + require.NoError(t, clientNamespaced.Delete(ctx, consumer)) + _ = watchFor(t, ctx, w, watch.Modified, + func(c *configurationv1alpha1.CredentialBasicAuth) bool { + return c.Name == credentialBasicAuth.Name + }, + "CredentialBasicAuth wasn't deleted but it should have been", + ) +} diff --git a/test/envtest/konnect_entities_suite_test.go b/test/envtest/konnect_entities_suite_test.go index d2ee92c8c..fa7399717 100644 --- a/test/envtest/konnect_entities_suite_test.go +++ b/test/envtest/konnect_entities_suite_test.go @@ -37,6 +37,7 @@ func TestKonnectEntityReconcilers(t *testing.T) { testNewKonnectEntityReconciler(t, cfg, configurationv1alpha1.KongRoute{}, nil) testNewKonnectEntityReconciler(t, cfg, configurationv1beta1.KongConsumerGroup{}, nil) testNewKonnectEntityReconciler(t, cfg, configurationv1alpha1.KongPluginBinding{}, nil) + testNewKonnectEntityReconciler(t, cfg, configurationv1alpha1.CredentialBasicAuth{}, nil) } type konnectEntityReconcilerTestCase struct {