diff --git a/.mockery.yaml b/.mockery.yaml index cdad1547e..662d84fd6 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -25,6 +25,7 @@ packages: KongCredentialACLSDK: KongCredentialBasicAuthSDK: KongCredentialJWTSDK: + KongCredentialHMACSDK: CACertificatesSDK: CertificatesSDK: KeysSDK: diff --git a/CHANGELOG.md b/CHANGELOG.md index fa4f60038..e425d24a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,6 +96,7 @@ - API key [#635](https://github.com/Kong/gateway-operator/pull/635) - ACL [#661](https://github.com/Kong/gateway-operator/pull/661) - JWT [#678](https://github.com/Kong/gateway-operator/pull/678) + - HMAC Auth [#687](https://github.com/Kong/gateway-operator/pull/687) - Add support for `KongRoute`s bound directly to `KonnectGatewayControlPlane`s (serviceless rotues). [#669](https://github.com/Kong/gateway-operator/pull/669) diff --git a/config/samples/konnect_kongconsumer_hmac.yaml b/config/samples/konnect_kongconsumer_hmac.yaml new file mode 100644 index 000000000..5bffc3ca4 --- /dev/null +++ b/config/samples/konnect_kongconsumer_hmac.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: consumer-hmac-1 + namespace: default +username: consumer1-hmac-1 +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: test-cp-basic-auth +--- +apiVersion: configuration.konghq.com/v1alpha1 +kind: KongCredentialHMAC +metadata: + name: hmac-1 + namespace: default +spec: + consumerRef: + name: consumer-hmac-1 + secret: secretkey + username: consumer1-hmac-1 diff --git a/controller/konnect/constraints/constraints.go b/controller/konnect/constraints/constraints.go index e6daeacb7..72ed4fab6 100644 --- a/controller/konnect/constraints/constraints.go +++ b/controller/konnect/constraints/constraints.go @@ -23,6 +23,7 @@ type SupportedKonnectEntityType interface { configurationv1alpha1.KongCredentialAPIKey | configurationv1alpha1.KongCredentialACL | configurationv1alpha1.KongCredentialJWT | + configurationv1alpha1.KongCredentialHMAC | configurationv1alpha1.KongUpstream | configurationv1alpha1.KongCACertificate | configurationv1alpha1.KongCertificate | diff --git a/controller/konnect/index_credentials_hmac.go b/controller/konnect/index_credentials_hmac.go new file mode 100644 index 000000000..afeea4073 --- /dev/null +++ b/controller/konnect/index_credentials_hmac.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 ( + // IndexFieldKongCredentialHMACReferencesKongConsumer is the index name for KongCredentialHMAC -> Consumer. + IndexFieldKongCredentialHMACReferencesKongConsumer = "kongCredentialsHMACConsumerRef" +) + +// IndexOptionsForCredentialsHMAC returns required Index options for KongCredentialHMAC. +func IndexOptionsForCredentialsHMAC() []ReconciliationIndexOption { + return []ReconciliationIndexOption{ + { + IndexObject: &configurationv1alpha1.KongCredentialHMAC{}, + IndexField: IndexFieldKongCredentialHMACReferencesKongConsumer, + ExtractValue: kongCredentialHMACReferencesConsumer, + }, + } +} + +// kongCredentialHMACReferencesConsumer returns the name of referenced Consumer. +func kongCredentialHMACReferencesConsumer(obj client.Object) []string { + cred, ok := obj.(*configurationv1alpha1.KongCredentialHMAC) + if !ok { + return nil + } + return []string{cred.Spec.ConsumerRef.Name} +} diff --git a/controller/konnect/ops/credentialhmac.go b/controller/konnect/ops/credentialhmac.go new file mode 100644 index 000000000..b2f95b076 --- /dev/null +++ b/controller/konnect/ops/credentialhmac.go @@ -0,0 +1,14 @@ +package ops + +import ( + "context" + + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// KongCredentialHMACSDK is the interface for the Konnect KongCredentialHMACSDK. +type KongCredentialHMACSDK interface { + CreateHmacAuthWithConsumer(ctx context.Context, req sdkkonnectops.CreateHmacAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateHmacAuthWithConsumerResponse, error) + DeleteHmacAuthWithConsumer(ctx context.Context, request sdkkonnectops.DeleteHmacAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteHmacAuthWithConsumerResponse, error) + UpsertHmacAuthWithConsumer(ctx context.Context, request sdkkonnectops.UpsertHmacAuthWithConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertHmacAuthWithConsumerResponse, error) +} diff --git a/controller/konnect/ops/credentialhmac_mock.go b/controller/konnect/ops/credentialhmac_mock.go new file mode 100644 index 000000000..d63f42e22 --- /dev/null +++ b/controller/konnect/ops/credentialhmac_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" +) + +// MockKongCredentialHMACSDK is an autogenerated mock type for the KongCredentialHMACSDK type +type MockKongCredentialHMACSDK struct { + mock.Mock +} + +type MockKongCredentialHMACSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockKongCredentialHMACSDK) EXPECT() *MockKongCredentialHMACSDK_Expecter { + return &MockKongCredentialHMACSDK_Expecter{mock: &_m.Mock} +} + +// CreateHmacAuthWithConsumer provides a mock function with given fields: ctx, req, opts +func (_m *MockKongCredentialHMACSDK) CreateHmacAuthWithConsumer(ctx context.Context, req operations.CreateHmacAuthWithConsumerRequest, opts ...operations.Option) (*operations.CreateHmacAuthWithConsumerResponse, 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 CreateHmacAuthWithConsumer") + } + + var r0 *operations.CreateHmacAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.CreateHmacAuthWithConsumerRequest, ...operations.Option) (*operations.CreateHmacAuthWithConsumerResponse, error)); ok { + return rf(ctx, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.CreateHmacAuthWithConsumerRequest, ...operations.Option) *operations.CreateHmacAuthWithConsumerResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateHmacAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.CreateHmacAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateHmacAuthWithConsumer' +type MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call struct { + *mock.Call +} + +// CreateHmacAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - req operations.CreateHmacAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockKongCredentialHMACSDK_Expecter) CreateHmacAuthWithConsumer(ctx interface{}, req interface{}, opts ...interface{}) *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call { + return &MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call{Call: _e.mock.On("CreateHmacAuthWithConsumer", + append([]interface{}{ctx, req}, opts...)...)} +} + +func (_c *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call) Run(run func(ctx context.Context, req operations.CreateHmacAuthWithConsumerRequest, opts ...operations.Option)) *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_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.CreateHmacAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call) Return(_a0 *operations.CreateHmacAuthWithConsumerResponse, _a1 error) *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.CreateHmacAuthWithConsumerRequest, ...operations.Option) (*operations.CreateHmacAuthWithConsumerResponse, error)) *MockKongCredentialHMACSDK_CreateHmacAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// DeleteHmacAuthWithConsumer provides a mock function with given fields: ctx, request, opts +func (_m *MockKongCredentialHMACSDK) DeleteHmacAuthWithConsumer(ctx context.Context, request operations.DeleteHmacAuthWithConsumerRequest, opts ...operations.Option) (*operations.DeleteHmacAuthWithConsumerResponse, 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 DeleteHmacAuthWithConsumer") + } + + var r0 *operations.DeleteHmacAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.DeleteHmacAuthWithConsumerRequest, ...operations.Option) (*operations.DeleteHmacAuthWithConsumerResponse, error)); ok { + return rf(ctx, request, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.DeleteHmacAuthWithConsumerRequest, ...operations.Option) *operations.DeleteHmacAuthWithConsumerResponse); ok { + r0 = rf(ctx, request, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteHmacAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.DeleteHmacAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, request, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteHmacAuthWithConsumer' +type MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call struct { + *mock.Call +} + +// DeleteHmacAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - request operations.DeleteHmacAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockKongCredentialHMACSDK_Expecter) DeleteHmacAuthWithConsumer(ctx interface{}, request interface{}, opts ...interface{}) *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call { + return &MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call{Call: _e.mock.On("DeleteHmacAuthWithConsumer", + append([]interface{}{ctx, request}, opts...)...)} +} + +func (_c *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call) Run(run func(ctx context.Context, request operations.DeleteHmacAuthWithConsumerRequest, opts ...operations.Option)) *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_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.DeleteHmacAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call) Return(_a0 *operations.DeleteHmacAuthWithConsumerResponse, _a1 error) *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.DeleteHmacAuthWithConsumerRequest, ...operations.Option) (*operations.DeleteHmacAuthWithConsumerResponse, error)) *MockKongCredentialHMACSDK_DeleteHmacAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// UpsertHmacAuthWithConsumer provides a mock function with given fields: ctx, request, opts +func (_m *MockKongCredentialHMACSDK) UpsertHmacAuthWithConsumer(ctx context.Context, request operations.UpsertHmacAuthWithConsumerRequest, opts ...operations.Option) (*operations.UpsertHmacAuthWithConsumerResponse, 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 UpsertHmacAuthWithConsumer") + } + + var r0 *operations.UpsertHmacAuthWithConsumerResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertHmacAuthWithConsumerRequest, ...operations.Option) (*operations.UpsertHmacAuthWithConsumerResponse, error)); ok { + return rf(ctx, request, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertHmacAuthWithConsumerRequest, ...operations.Option) *operations.UpsertHmacAuthWithConsumerResponse); ok { + r0 = rf(ctx, request, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpsertHmacAuthWithConsumerResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.UpsertHmacAuthWithConsumerRequest, ...operations.Option) error); ok { + r1 = rf(ctx, request, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertHmacAuthWithConsumer' +type MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call struct { + *mock.Call +} + +// UpsertHmacAuthWithConsumer is a helper method to define mock.On call +// - ctx context.Context +// - request operations.UpsertHmacAuthWithConsumerRequest +// - opts ...operations.Option +func (_e *MockKongCredentialHMACSDK_Expecter) UpsertHmacAuthWithConsumer(ctx interface{}, request interface{}, opts ...interface{}) *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call { + return &MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call{Call: _e.mock.On("UpsertHmacAuthWithConsumer", + append([]interface{}{ctx, request}, opts...)...)} +} + +func (_c *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call) Run(run func(ctx context.Context, request operations.UpsertHmacAuthWithConsumerRequest, opts ...operations.Option)) *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_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.UpsertHmacAuthWithConsumerRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call) Return(_a0 *operations.UpsertHmacAuthWithConsumerResponse, _a1 error) *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call) RunAndReturn(run func(context.Context, operations.UpsertHmacAuthWithConsumerRequest, ...operations.Option) (*operations.UpsertHmacAuthWithConsumerResponse, error)) *MockKongCredentialHMACSDK_UpsertHmacAuthWithConsumer_Call { + _c.Call.Return(run) + return _c +} + +// NewMockKongCredentialHMACSDK creates a new instance of MockKongCredentialHMACSDK. 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 NewMockKongCredentialHMACSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockKongCredentialHMACSDK { + mock := &MockKongCredentialHMACSDK{} + 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 b61dca653..26afbf7f0 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -74,6 +74,8 @@ func Create[ return e, createKongCredentialACL(ctx, sdk.GetACLCredentialsSDK(), ent) case *configurationv1alpha1.KongCredentialJWT: return e, createKongCredentialJWT(ctx, sdk.GetJWTCredentialsSDK(), ent) + case *configurationv1alpha1.KongCredentialHMAC: + return e, createKongCredentialHMAC(ctx, sdk.GetHMACCredentialsSDK(), ent) case *configurationv1alpha1.KongCACertificate: return e, createCACertificate(ctx, sdk.GetCACertificatesSDK(), ent) case *configurationv1alpha1.KongCertificate: @@ -136,6 +138,8 @@ func Delete[ return deleteKongCredentialACL(ctx, sdk.GetACLCredentialsSDK(), ent) case *configurationv1alpha1.KongCredentialJWT: return deleteKongCredentialJWT(ctx, sdk.GetJWTCredentialsSDK(), ent) + case *configurationv1alpha1.KongCredentialHMAC: + return deleteKongCredentialHMAC(ctx, sdk.GetHMACCredentialsSDK(), ent) case *configurationv1alpha1.KongCACertificate: return deleteCACertificate(ctx, sdk.GetCACertificatesSDK(), ent) case *configurationv1alpha1.KongCertificate: @@ -243,6 +247,8 @@ func Update[ return ctrl.Result{}, updateKongCredentialACL(ctx, sdk.GetACLCredentialsSDK(), ent) case *configurationv1alpha1.KongCredentialJWT: return ctrl.Result{}, updateKongCredentialJWT(ctx, sdk.GetJWTCredentialsSDK(), ent) + case *configurationv1alpha1.KongCredentialHMAC: + return ctrl.Result{}, updateKongCredentialHMAC(ctx, sdk.GetHMACCredentialsSDK(), ent) case *configurationv1alpha1.KongCACertificate: return ctrl.Result{}, updateCACertificate(ctx, sdk.GetCACertificatesSDK(), ent) case *configurationv1alpha1.KongCertificate: diff --git a/controller/konnect/ops/ops_credentialhmac.go b/controller/konnect/ops/ops_credentialhmac.go new file mode 100644 index 000000000..b5dd6f0b8 --- /dev/null +++ b/controller/konnect/ops/ops_credentialhmac.go @@ -0,0 +1,155 @@ +package ops + +import ( + "context" + "errors" + "fmt" + + 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" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +func createKongCredentialHMAC( + ctx context.Context, + sdk KongCredentialHMACSDK, + cred *configurationv1alpha1.KongCredentialHMAC, +) 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.CreateHmacAuthWithConsumer(ctx, + sdkkonnectops.CreateHmacAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + HMACAuthWithoutParents: kongCredentialHMACToHMACWithoutParents(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 errWrap := wrapErrIfKonnectOpFailed(err, CreateOp, cred); errWrap != nil { + SetKonnectEntityProgrammedConditionFalse(cred, "FailedToCreate", errWrap.Error()) + return errWrap + } + + cred.Status.Konnect.SetKonnectID(*resp.HMACAuth.ID) + SetKonnectEntityProgrammedCondition(cred) + + return nil +} + +// updateKongCredentialHMAC updates the Konnect HMAC entity. +// It is assumed that the provided HMAC has Konnect ID set in status. +// It returns an error if the HMAC does not have a ControlPlaneRef or +// if the operation fails. +func updateKongCredentialHMAC( + ctx context.Context, + sdk KongCredentialHMACSDK, + cred *configurationv1alpha1.KongCredentialHMAC, +) 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.UpsertHmacAuthWithConsumer(ctx, + sdkkonnectops.UpsertHmacAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + HMACAuthID: cred.GetKonnectStatus().GetKonnectID(), + HMACAuthWithoutParents: kongCredentialHMACToHMACWithoutParents(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 errWrap := wrapErrIfKonnectOpFailed(err, UpdateOp, cred); errWrap != nil { + // HMAC update operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrap, &sdkError) { + switch sdkError.StatusCode { + case 404: + if err := createKongCredentialHMAC(ctx, sdk, cred); err != nil { + return FailedKonnectOpError[configurationv1alpha1.KongCredentialHMAC]{ + Op: UpdateOp, + Err: err, + } + } + return nil + default: + return FailedKonnectOpError[configurationv1alpha1.KongCredentialHMAC]{ + Op: UpdateOp, + Err: sdkError, + } + + } + } + + SetKonnectEntityProgrammedConditionFalse(cred, "FailedToUpdate", errWrap.Error()) + return errWrap + } + + SetKonnectEntityProgrammedCondition(cred) + + return nil +} + +// deleteKongCredentialHMAC deletes an HMAC credential in Konnect. +// It is assumed that the provided HMAC has Konnect ID set in status. +// It returns an error if the operation fails. +func deleteKongCredentialHMAC( + ctx context.Context, + sdk KongCredentialHMACSDK, + cred *configurationv1alpha1.KongCredentialHMAC, +) error { + cpID := cred.GetControlPlaneID() + id := cred.GetKonnectStatus().GetKonnectID() + _, err := sdk.DeleteHmacAuthWithConsumer(ctx, + sdkkonnectops.DeleteHmacAuthWithConsumerRequest{ + ControlPlaneID: cpID, + ConsumerIDForNestedEntities: cred.Status.Konnect.GetConsumerID(), + HMACAuthID: id, + }) + if errWrap := wrapErrIfKonnectOpFailed(err, DeleteOp, cred); errWrap != nil { + // Service delete operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrap, &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.KongCredentialHMAC]{ + Op: DeleteOp, + Err: sdkError, + } + } + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: DeleteOp, + Err: errWrap, + } + } + + return nil +} + +func kongCredentialHMACToHMACWithoutParents( + cred *configurationv1alpha1.KongCredentialHMAC, +) sdkkonnectcomp.HMACAuthWithoutParents { + ret := sdkkonnectcomp.HMACAuthWithoutParents{ + Username: cred.Spec.Username, + Secret: cred.Spec.Secret, + Tags: GenerateTagsForObject(cred, cred.Spec.Tags...), + } + return ret +} diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index c92be2d9c..7504f00a0 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -21,6 +21,7 @@ type SDKWrapper interface { GetAPIKeyCredentialsSDK() KongCredentialAPIKeySDK GetACLCredentialsSDK() KongCredentialACLSDK GetJWTCredentialsSDK() KongCredentialJWTSDK + GetHMACCredentialsSDK() KongCredentialHMACSDK GetCACertificatesSDK() CACertificatesSDK GetCertificatesSDK() CertificatesSDK GetKeysSDK() KeysSDK @@ -120,6 +121,11 @@ func (w sdkWrapper) GetJWTCredentialsSDK() KongCredentialJWTSDK { return w.sdk.JWTs } +// GetHMACCredentialsSDK returns the HMACCredentials SDK to get current organization. +func (w sdkWrapper) GetHMACCredentialsSDK() KongCredentialHMACSDK { + return w.sdk.HMACAuthCredentials +} + // GetKeysSDK returns the SDK to operate keys. func (w sdkWrapper) GetKeysSDK() KeysSDK { return w.sdk.Keys diff --git a/controller/konnect/ops/sdkfactory_mock.go b/controller/konnect/ops/sdkfactory_mock.go index 250112e10..fd17d37b0 100644 --- a/controller/konnect/ops/sdkfactory_mock.go +++ b/controller/konnect/ops/sdkfactory_mock.go @@ -20,6 +20,7 @@ type MockSDKWrapper struct { KongCredentialsAPIKeySDK *MockKongCredentialAPIKeySDK KongCredentialsACLSDK *MockKongCredentialACLSDK KongCredentialsJWTSDK *MockKongCredentialJWTSDK + KongCredentialsHMACSDK *MockKongCredentialHMACSDK CACertificatesSDK *MockCACertificatesSDK CertificatesSDK *MockCertificatesSDK VaultSDK *MockVaultSDK @@ -46,6 +47,7 @@ func NewMockSDKWrapperWithT(t *testing.T) *MockSDKWrapper { KongCredentialsAPIKeySDK: NewMockKongCredentialAPIKeySDK(t), KongCredentialsACLSDK: NewMockKongCredentialACLSDK(t), KongCredentialsJWTSDK: NewMockKongCredentialJWTSDK(t), + KongCredentialsHMACSDK: NewMockKongCredentialHMACSDK(t), CACertificatesSDK: NewMockCACertificatesSDK(t), CertificatesSDK: NewMockCertificatesSDK(t), VaultSDK: NewMockVaultSDK(t), @@ -100,6 +102,10 @@ func (m MockSDKWrapper) GetJWTCredentialsSDK() KongCredentialJWTSDK { return m.KongCredentialsJWTSDK } +func (m MockSDKWrapper) GetHMACCredentialsSDK() KongCredentialHMACSDK { + return m.KongCredentialsHMACSDK +} + func (m MockSDKWrapper) GetTargetsSDK() TargetsSDK { return m.TargetsSDK } diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index db567905f..ce99c8312 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -758,6 +758,8 @@ func getConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constraints.E return mo.Some(e.Spec.ConsumerRef) case *configurationv1alpha1.KongCredentialJWT: return mo.Some(e.Spec.ConsumerRef) + case *configurationv1alpha1.KongCredentialHMAC: + return mo.Some(e.Spec.ConsumerRef) default: return mo.None[corev1.LocalObjectReference]() } @@ -860,6 +862,12 @@ func handleKongConsumerRef[T constraints.SupportedKonnectEntityType, TEnt constr } cred.Status.Konnect.ConsumerID = consumer.Status.Konnect.GetKonnectID() } + if cred, ok := any(ent).(*configurationv1alpha1.KongCredentialHMAC); 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, diff --git a/controller/konnect/watch.go b/controller/konnect/watch.go index 0442c6432..f4d14ddc2 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -51,6 +51,8 @@ func ReconciliationWatchOptionsForEntity[ return kongCredentialACLReconciliationWatchOptions(cl) case *configurationv1alpha1.KongCredentialJWT: return kongCredentialJWTReconciliationWatchOptions(cl) + case *configurationv1alpha1.KongCredentialHMAC: + return kongCredentialHMACReconciliationWatchOptions(cl) case *configurationv1alpha1.KongCACertificate: return KongCACertificateReconciliationWatchOptions(cl) case *configurationv1alpha1.KongCertificate: diff --git a/controller/konnect/watch_credential.go b/controller/konnect/watch_credential.go index 4132d375f..21373642d 100644 --- a/controller/konnect/watch_credential.go +++ b/controller/konnect/watch_credential.go @@ -23,8 +23,8 @@ func kongCredentialRefersToKonnectGatewayControlPlane[ *configurationv1alpha1.KongCredentialACL | *configurationv1alpha1.KongCredentialAPIKey | *configurationv1alpha1.KongCredentialBasicAuth | - *configurationv1alpha1.KongCredentialJWT - // TODO add support for HMAC Auth https://github.com/Kong/gateway-operator/issues/621 + *configurationv1alpha1.KongCredentialJWT | + *configurationv1alpha1.KongCredentialHMAC GetTypeName() string GetNamespace() string @@ -51,6 +51,8 @@ func kongCredentialRefersToKonnectGatewayControlPlane[ consumerRefName = credential.Spec.ConsumerRef.Name case *configurationv1alpha1.KongCredentialJWT: consumerRefName = credential.Spec.ConsumerRef.Name + case *configurationv1alpha1.KongCredentialHMAC: + consumerRefName = credential.Spec.ConsumerRef.Name } nn := types.NamespacedName{ diff --git a/controller/konnect/watch_credentialhmac.go b/controller/konnect/watch_credentialhmac.go new file mode 100644 index 000000000..3fa5aacc7 --- /dev/null +++ b/controller/konnect/watch_credentialhmac.go @@ -0,0 +1,210 @@ +package konnect + +import ( + "context" + + "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" + + 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 + +// kongCredentialHMACReconciliationWatchOptions returns the watch options for +// the KongCredentialHMAC resource. +func kongCredentialHMACReconciliationWatchOptions( + cl client.Client, +) []func(*ctrl.Builder) *ctrl.Builder { + return []func(*ctrl.Builder) *ctrl.Builder{ + func(b *ctrl.Builder) *ctrl.Builder { + return b.For(&configurationv1alpha1.KongCredentialHMAC{}, + builder.WithPredicates( + predicate.NewPredicateFuncs( + kongCredentialRefersToKonnectGatewayControlPlane[*configurationv1alpha1.KongCredentialHMAC](cl), + ), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &configurationv1.KongConsumer{}, + handler.EnqueueRequestsFromMapFunc( + kongCredentialHMACForKongConsumer(cl), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectAPIAuthConfiguration{}, + handler.EnqueueRequestsFromMapFunc( + kongCredentialHMACForKonnectAPIAuthConfiguration(cl), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectGatewayControlPlane{}, + handler.EnqueueRequestsFromMapFunc( + kongCredentialHMACForKonnectGatewayControlPlane(cl), + ), + ) + }, + } +} + +func kongCredentialHMACForKonnectAPIAuthConfiguration( + 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.KongCredentialHMACList + if err := cl.List(ctx, &credList, + client.MatchingFields{ + IndexFieldKongCredentialHMACReferencesKongConsumer: 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 kongCredentialHMACForKonnectGatewayControlPlane( + 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.KongCredentialHMACList + if err := cl.List(ctx, &credList, + client.MatchingFields{ + IndexFieldKongCredentialHMACReferencesKongConsumer: 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 kongCredentialHMACForKongConsumer( + 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.KongCredentialHMACList + if err := cl.List(ctx, &l, + client.MatchingFields{ + IndexFieldKongCredentialHMACReferencesKongConsumer: consumer.Name, + }, + // TODO: change this when cross namespace refs are allowed. + client.InNamespace(consumer.GetNamespace()), + ); err != nil { + return nil + } + + return objectListToReconcileRequests(l.Items) + } +} diff --git a/modules/manager/controller_setup.go b/modules/manager/controller_setup.go index 3c9793c55..8d38c02dd 100644 --- a/modules/manager/controller_setup.go +++ b/modules/manager/controller_setup.go @@ -90,6 +90,8 @@ const ( KongCredentialAPIKeyControllerName = "KongCredentialAPIKey" //nolint:gosec // KongCredentialACLControllerName is the name of the KongCredentialACL controller. KongCredentialACLControllerName = "KongCredentialACL" //nolint:gosec + // KongCredentialHMACControllerName is the name of the KongCredentialHMAC controller. + KongCredentialHMACControllerName = "KongCredentialHMAC" //nolint:gosec // KongCredentialJWTControllerName is the name of the KongCredentialJWT controller. KongCredentialJWTControllerName = "KongCredentialJWT" //nolint:gosec // KongCACertificateControllerName is the name of the KongCACertificate controller. @@ -488,6 +490,15 @@ func SetupControllers(mgr manager.Manager, c *Config) (map[string]ControllerDef, konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongCredentialACL](c.KonnectSyncPeriod), ), }, + KongCredentialHMACControllerName: { + Enabled: c.KonnectControllersEnabled, + Controller: konnect.NewKonnectEntityReconciler( + sdkFactory, + c.DevelopmentMode, + mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongCredentialHMAC](c.KonnectSyncPeriod), + ), + }, KongCredentialJWTControllerName: { Enabled: c.KonnectControllersEnabled, Controller: konnect.NewKonnectEntityReconciler( @@ -603,6 +614,10 @@ func SetupCacheIndicesForKonnectTypes(ctx context.Context, mgr manager.Manager, Object: &configurationv1alpha1.KongCredentialAPIKey{}, IndexOptions: konnect.IndexOptionsForCredentialsAPIKey(), }, + { + Object: &configurationv1alpha1.KongCredentialHMAC{}, + IndexOptions: konnect.IndexOptionsForCredentialsHMAC(), + }, { Object: &configurationv1.KongConsumer{}, IndexOptions: konnect.IndexOptionsForKongConsumer(), diff --git a/test/envtest/kongconsumercredential_hmac_test.go b/test/envtest/kongconsumercredential_hmac_test.go new file mode 100644 index 000000000..30d6abf0b --- /dev/null +++ b/test/envtest/kongconsumercredential_hmac_test.go @@ -0,0 +1,163 @@ +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" + "github.com/kong/gateway-operator/test/helpers/deploy" + + 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_HMAC(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 := deploy.KonnectAPIAuthConfigurationWithProgrammed(t, ctx, clientNamespaced) + cp := deploy.KonnectGatewayControlPlaneWithID(t, ctx, clientNamespaced, apiAuth) + + consumerID := uuid.NewString() + consumer := deploy.KongConsumerWithProgrammed(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)) + + kongCredentialHMAC := deploy.KongCredentialHMAC(t, ctx, clientNamespaced, consumer.Name) + hmacID := uuid.NewString() + tags := []string{ + "k8s-generation:1", + "k8s-group:configuration.konghq.com", + "k8s-kind:KongCredentialHMAC", + "k8s-name:" + kongCredentialHMAC.Name, + "k8s-namespace:" + ns.Name, + "k8s-uid:" + string(kongCredentialHMAC.GetUID()), + "k8s-version:v1alpha1", + } + + factory := ops.NewMockSDKFactory(t) + factory.SDK.KongCredentialsHMACSDK.EXPECT(). + CreateHmacAuthWithConsumer( + mock.Anything, + sdkkonnectops.CreateHmacAuthWithConsumerRequest{ + ControlPlaneID: cp.GetKonnectStatus().GetKonnectID(), + ConsumerIDForNestedEntities: consumerID, + HMACAuthWithoutParents: sdkkonnectcomp.HMACAuthWithoutParents{ + Username: lo.ToPtr("username"), + Tags: tags, + }, + }, + ). + Return( + &sdkkonnectops.CreateHmacAuthWithConsumerResponse{ + HMACAuth: &sdkkonnectcomp.HMACAuth{ + ID: lo.ToPtr(hmacID), + }, + }, + nil, + ) + factory.SDK.KongCredentialsHMACSDK.EXPECT(). + UpsertHmacAuthWithConsumer(mock.Anything, mock.Anything, mock.Anything).Maybe(). + Return( + &sdkkonnectops.UpsertHmacAuthWithConsumerResponse{ + HMACAuth: &sdkkonnectcomp.HMACAuth{ + ID: lo.ToPtr(hmacID), + }, + }, + nil, + ) + + require.NoError(t, manager.SetupCacheIndicesForKonnectTypes(ctx, mgr, false)) + reconcilers := []Reconciler{ + konnect.NewKonnectEntityReconciler(factory, false, mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongCredentialHMAC](konnectSyncTime), + ), + } + + StartReconcilers(ctx, t, mgr, logs, reconcilers...) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.KongCredentialsHMACSDK.AssertExpectations(t)) + }, waitTime, tickTime) + + factory.SDK.KongCredentialsHMACSDK.EXPECT(). + DeleteHmacAuthWithConsumer( + mock.Anything, + sdkkonnectops.DeleteHmacAuthWithConsumerRequest{ + ControlPlaneID: cp.GetKonnectStatus().GetKonnectID(), + ConsumerIDForNestedEntities: consumerID, + HMACAuthID: hmacID, + }, + ). + Return( + &sdkkonnectops.DeleteHmacAuthWithConsumerResponse{ + StatusCode: 200, + }, + nil, + ) + require.NoError(t, clientNamespaced.Delete(ctx, kongCredentialHMAC)) + + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.KongCredentialsHMACSDK.AssertExpectations(t)) + }, waitTime, tickTime) + + w := setupWatch[configurationv1alpha1.KongCredentialHMACList](t, ctx, clientWithWatch, client.InNamespace(ns.Name)) + + kongCredentialHMAC = deploy.KongCredentialHMAC(t, ctx, clientNamespaced, consumer.Name) + t.Logf("redeployed %s KongCredentialHMAC resource", client.ObjectKeyFromObject(kongCredentialHMAC)) + t.Logf("checking if KongConsumer %s removal will delete the associated credentials %s", + client.ObjectKeyFromObject(consumer), + client.ObjectKeyFromObject(kongCredentialHMAC), + ) + + require.NoError(t, clientNamespaced.Delete(ctx, consumer)) + _ = watchFor(t, ctx, w, watch.Modified, + func(c *configurationv1alpha1.KongCredentialHMAC) bool { + return c.Name == kongCredentialHMAC.Name + }, + "KongCredentialHMAC wasn't deleted but it should have been", + ) +} diff --git a/test/helpers/deploy/deploy_resources.go b/test/helpers/deploy/deploy_resources.go index bcdbd9fdc..b8df7bb61 100644 --- a/test/helpers/deploy/deploy_resources.go +++ b/test/helpers/deploy/deploy_resources.go @@ -364,6 +364,35 @@ func KongCredentialACL( return c } +// KongCredentialHMAC deploys a KongCredentialHMAC resource and returns the resource. +func KongCredentialHMAC( + t *testing.T, + ctx context.Context, + cl client.Client, + consumerName string, +) *configurationv1alpha1.KongCredentialHMAC { + t.Helper() + + c := &configurationv1alpha1.KongCredentialHMAC{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "hmac-", + }, + Spec: configurationv1alpha1.KongCredentialHMACSpec{ + ConsumerRef: corev1.LocalObjectReference{ + Name: consumerName, + }, + KongCredentialHMACAPISpec: configurationv1alpha1.KongCredentialHMACAPISpec{ + Username: lo.ToPtr("username"), + }, + }, + } + + require.NoError(t, cl.Create(ctx, c)) + t.Logf("deployed new unmanaged KongCredentialHMAC %s", client.ObjectKeyFromObject(c)) + + return c +} + // KongCredentialJWT deploys a KongCredentialJWT resource and returns the resource. func KongCredentialJWT( t *testing.T,