diff --git a/.golangci.yaml b/.golangci.yaml index efd788cb6..5547da8a3 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -151,3 +151,7 @@ issues: linters: - revive text: "exported: exported" + # Methods imported from the SDK do not use the camel case naming convention for DP and we have no control over it. + - linters: + - forbidigo + text: 'use of `.*(Create|Delete)Dataplane.+` forbidden because "Please use camel case' diff --git a/.mockery.yaml b/.mockery.yaml index 4215a1f75..cdad1547e 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -30,3 +30,4 @@ packages: KeysSDK: KeySetsSDK: SNIsSDK: + DataPlaneClientCertificatesSDK: diff --git a/CHANGELOG.md b/CHANGELOG.md index e9127c8cf..934635be1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,6 +68,8 @@ [#646](https://github.com/Kong/gateway-operator/pull/646) - Add `KongKeySet` reconciler for Konnect KeySets. [#657](https://github.com/Kong/gateway-operator/pull/657) +- Add `KongDataPlaneClientCertificate` reconciler for Konnect DataPlaneClientCertificates. + [#694](https://github.com/Kong/gateway-operator/pull/694) - The `DataPlaneKonnectExtension` CRD has been introduced. Such a CRD can be attached to a `DataPlane` via the extensions field to have a konnect-flavored `DataPlane`. [#453](https://github.com/Kong/gateway-operator/pull/453), [#578](https://github.com/Kong/gateway-operator/pull/578) diff --git a/config/samples/konnect_kongdataplaneclientcertificate.yaml b/config/samples/konnect_kongdataplaneclientcertificate.yaml new file mode 100644 index 000000000..c9be23aff --- /dev/null +++ b/config/samples/konnect_kongdataplaneclientcertificate.yaml @@ -0,0 +1,57 @@ +kind: KonnectAPIAuthConfiguration +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: konnect-api-auth-dev-1 + namespace: default +spec: + type: token + token: kpat_XXXXXXXXXXXXXXXXXXX + serverURL: us.api.konghq.tech +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: test1 + namespace: default +spec: + name: test1 + labels: + app: test1 + key1: test1 + konnect: + authRef: + name: konnect-api-auth-dev-1 +--- +kind: KongDataPlaneClientCertificate +apiVersion: configuration.konghq.com/v1alpha1 +metadata: + name: dp-cert-1 + namespace: default + annotations: + konghq.com/tags: "infra" +spec: + controlPlaneRef: + type: konnectNamespacedRef + konnectNamespacedRef: + name: test1 + cert: | + -----BEGIN CERTIFICATE----- + MIIDPTCCAiWgAwIBAgIUcNKAk2icWRJGwZ5QDpdSkkeF5kUwDQYJKoZIhvcNAQEL + BQAwLjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQKDAlLb25nIElu + Yy4wHhcNMjQwOTE5MDkwODEzWhcNMjkwOTE4MDkwODEzWjAuMQswCQYDVQQGEwJV + UzELMAkGA1UECAwCQ0ExEjAQBgNVBAoMCUtvbmcgSW5jLjCCASIwDQYJKoZIhvcN + AQEBBQADggEPADCCAQoCggEBAMvDhLM0vTw0QXmgE+sB6gvKx2PUWzvd2tRZoamH + h4RAxYRjgJsJe6WEeAk0tjWQqwAq0Y2MQioMCC4X+L13kpdtomI+4PKjBozg+iTd + ThyV0oQSVHHWzayUzcSODnGR524H9YxmkXV5ImrXwbEqXwiUESPVtjnf/ZzWS01v + gtbu4x3YW+z8kRoXOTpJHKcEoI90SU9F4yeuQsCtbJHeJZRqPr6Kz84ZuHsZ2MeU + os4j1GdMaH3dSysqFv6o1hJ2+6bsrE/ONiGtBb4+tyhivgf+u+ixQwqIERlEJzhI + z/csoAAnfMBY401j2NNUgPpwx5sTQdCz5aFDmanol5152M8CAwEAAaNTMFEwHQYD + VR0OBBYEFK2qd3oRF37acVvgfDeLakx66ioTMB8GA1UdIwQYMBaAFK2qd3oRF37a + cVvgfDeLakx66ioTMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB + AAuul+rAztaueTpPIM63nrS4bSZsIatCgAQ5Pihm0+rZ+13BJk4K2GxkS+T0qkB5 + 34+F3eVhUB4cC+kVkWZrlEzD9BsJwWjnoJK+848znTg+ufTeaOQWslYNqFKjmy2k + K6NE7E6r+JLdNvafJzeDybSTXI1tCzDRWUdj5m+bgruX07B13KIJKrAweCTD1927 + WvvfJYxsg8P7dYD9DPlcuOm22ggAaPPu4P/MsnApiq3kJEI/nSGSsboKyjBO2hcz + VF1CYr6Epfyw/47kwuJLCVHjlTgT4haOChW1S8rZILCLXfb8ukM/g3XVYIeEwzsr + KU74cm8lTFCdxlcXePbMdHc= + -----END CERTIFICATE----- diff --git a/controller/konnect/constraints/constraints.go b/controller/konnect/constraints/constraints.go index 9969403eb..e6daeacb7 100644 --- a/controller/konnect/constraints/constraints.go +++ b/controller/konnect/constraints/constraints.go @@ -30,7 +30,8 @@ type SupportedKonnectEntityType interface { configurationv1alpha1.KongVault | configurationv1alpha1.KongKey | configurationv1alpha1.KongKeySet | - configurationv1alpha1.KongSNI + configurationv1alpha1.KongSNI | + configurationv1alpha1.KongDataPlaneClientCertificate // TODO: add other types GetTypeName() string diff --git a/controller/konnect/index_kongdataplanecertificate.go b/controller/konnect/index_kongdataplanecertificate.go new file mode 100644 index 000000000..fee351ce0 --- /dev/null +++ b/controller/konnect/index_kongdataplanecertificate.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 ( + // IndexFieldKongDataPlaneClientCertificateOnKonnectGatewayControlPlane is the index field for KongDataPlaneCertificate -> KonnectGatewayControlPlane. + IndexFieldKongDataPlaneClientCertificateOnKonnectGatewayControlPlane = "dataPlaneCertificateKonnectGatewayControlPlaneRef" +) + +// IndexOptionsForKongDataPlaneCertificate returns required Index options for KongConsumer reconciler. +func IndexOptionsForKongDataPlaneCertificate() []ReconciliationIndexOption { + return []ReconciliationIndexOption{ + { + IndexObject: &configurationv1alpha1.KongDataPlaneClientCertificate{}, + IndexField: IndexFieldKongDataPlaneClientCertificateOnKonnectGatewayControlPlane, + ExtractValue: kongDataPlaneCertificateReferencesKonnectGatewayControlPlane, + }, + } +} + +func kongDataPlaneCertificateReferencesKonnectGatewayControlPlane(object client.Object) []string { + dpCert, ok := object.(*configurationv1alpha1.KongDataPlaneClientCertificate) + if !ok { + return nil + } + + return controlPlaneKonnectNamespacedRefAsSlice(dpCert) +} diff --git a/controller/konnect/ops/kongdataplanecertificate.go b/controller/konnect/ops/kongdataplanecertificate.go new file mode 100644 index 000000000..b77fdd837 --- /dev/null +++ b/controller/konnect/ops/kongdataplanecertificate.go @@ -0,0 +1,14 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// DataPlaneClientCertificatesSDK is the interface for the DataPlaneClientCertificatesSDK. +type DataPlaneClientCertificatesSDK interface { + CreateDataplaneCertificate(ctx context.Context, cpID string, dpReq *sdkkonnectcomp.DataPlaneClientCertificateRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateDataplaneCertificateResponse, error) + DeleteDataplaneCertificate(ctx context.Context, controlPlaneID string, certificateID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteDataplaneCertificateResponse, error) +} diff --git a/controller/konnect/ops/kongdataplanecertificate_mock.go b/controller/konnect/ops/kongdataplanecertificate_mock.go new file mode 100644 index 000000000..09b768a76 --- /dev/null +++ b/controller/konnect/ops/kongdataplanecertificate_mock.go @@ -0,0 +1,190 @@ +// Code generated by mockery. DO NOT EDIT. + +package ops + +import ( + context "context" + + components "github.com/Kong/sdk-konnect-go/models/components" + + mock "github.com/stretchr/testify/mock" + + operations "github.com/Kong/sdk-konnect-go/models/operations" +) + +// MockDataPlaneClientCertificatesSDK is an autogenerated mock type for the DataPlaneClientCertificatesSDK type +type MockDataPlaneClientCertificatesSDK struct { + mock.Mock +} + +type MockDataPlaneClientCertificatesSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockDataPlaneClientCertificatesSDK) EXPECT() *MockDataPlaneClientCertificatesSDK_Expecter { + return &MockDataPlaneClientCertificatesSDK_Expecter{mock: &_m.Mock} +} + +// CreateDataplaneCertificate provides a mock function with given fields: ctx, cpID, dpReq, opts +func (_m *MockDataPlaneClientCertificatesSDK) CreateDataplaneCertificate(ctx context.Context, cpID string, dpReq *components.DataPlaneClientCertificateRequest, opts ...operations.Option) (*operations.CreateDataplaneCertificateResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, cpID, dpReq) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateDataplaneCertificate") + } + + var r0 *operations.CreateDataplaneCertificateResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *components.DataPlaneClientCertificateRequest, ...operations.Option) (*operations.CreateDataplaneCertificateResponse, error)); ok { + return rf(ctx, cpID, dpReq, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *components.DataPlaneClientCertificateRequest, ...operations.Option) *operations.CreateDataplaneCertificateResponse); ok { + r0 = rf(ctx, cpID, dpReq, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateDataplaneCertificateResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *components.DataPlaneClientCertificateRequest, ...operations.Option) error); ok { + r1 = rf(ctx, cpID, dpReq, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateDataplaneCertificate' +type MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call struct { + *mock.Call +} + +// CreateDataplaneCertificate is a helper method to define mock.On call +// - ctx context.Context +// - cpID string +// - dpReq *components.DataPlaneClientCertificateRequest +// - opts ...operations.Option +func (_e *MockDataPlaneClientCertificatesSDK_Expecter) CreateDataplaneCertificate(ctx interface{}, cpID interface{}, dpReq interface{}, opts ...interface{}) *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call { + return &MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call{Call: _e.mock.On("CreateDataplaneCertificate", + append([]interface{}{ctx, cpID, dpReq}, opts...)...)} +} + +func (_c *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call) Run(run func(ctx context.Context, cpID string, dpReq *components.DataPlaneClientCertificateRequest, opts ...operations.Option)) *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(*components.DataPlaneClientCertificateRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call) Return(_a0 *operations.CreateDataplaneCertificateResponse, _a1 error) *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call) RunAndReturn(run func(context.Context, string, *components.DataPlaneClientCertificateRequest, ...operations.Option) (*operations.CreateDataplaneCertificateResponse, error)) *MockDataPlaneClientCertificatesSDK_CreateDataplaneCertificate_Call { + _c.Call.Return(run) + return _c +} + +// DeleteDataplaneCertificate provides a mock function with given fields: ctx, controlPlaneID, certificateID, opts +func (_m *MockDataPlaneClientCertificatesSDK) DeleteDataplaneCertificate(ctx context.Context, controlPlaneID string, certificateID string, opts ...operations.Option) (*operations.DeleteDataplaneCertificateResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, certificateID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteDataplaneCertificate") + } + + var r0 *operations.DeleteDataplaneCertificateResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) (*operations.DeleteDataplaneCertificateResponse, error)); ok { + return rf(ctx, controlPlaneID, certificateID, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) *operations.DeleteDataplaneCertificateResponse); ok { + r0 = rf(ctx, controlPlaneID, certificateID, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteDataplaneCertificateResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, certificateID, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteDataplaneCertificate' +type MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call struct { + *mock.Call +} + +// DeleteDataplaneCertificate is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - certificateID string +// - opts ...operations.Option +func (_e *MockDataPlaneClientCertificatesSDK_Expecter) DeleteDataplaneCertificate(ctx interface{}, controlPlaneID interface{}, certificateID interface{}, opts ...interface{}) *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call { + return &MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call{Call: _e.mock.On("DeleteDataplaneCertificate", + append([]interface{}{ctx, controlPlaneID, certificateID}, opts...)...)} +} + +func (_c *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call) Run(run func(ctx context.Context, controlPlaneID string, certificateID string, opts ...operations.Option)) *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call) Return(_a0 *operations.DeleteDataplaneCertificateResponse, _a1 error) *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call) RunAndReturn(run func(context.Context, string, string, ...operations.Option) (*operations.DeleteDataplaneCertificateResponse, error)) *MockDataPlaneClientCertificatesSDK_DeleteDataplaneCertificate_Call { + _c.Call.Return(run) + return _c +} + +// NewMockDataPlaneClientCertificatesSDK creates a new instance of MockDataPlaneClientCertificatesSDK. 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 NewMockDataPlaneClientCertificatesSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockDataPlaneClientCertificatesSDK { + mock := &MockDataPlaneClientCertificatesSDK{} + 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 f6e6e3db3..b61dca653 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -88,10 +88,10 @@ func Create[ return e, createKeySet(ctx, sdk.GetKeySetsSDK(), ent) case *configurationv1alpha1.KongSNI: return e, createSNI(ctx, sdk.GetSNIsSDK(), ent) - + case *configurationv1alpha1.KongDataPlaneClientCertificate: + return e, createKongDataPlaneClientCertificate(ctx, sdk.GetDataPlaneCertificatesSDK(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types - default: return nil, fmt.Errorf("unsupported entity type %T", ent) } @@ -150,10 +150,10 @@ func Delete[ return deleteKeySet(ctx, sdk.GetKeySetsSDK(), ent) case *configurationv1alpha1.KongSNI: return deleteSNI(ctx, sdk.GetSNIsSDK(), ent) - + case *configurationv1alpha1.KongDataPlaneClientCertificate: + return deleteKongDataPlaneClientCertificate(ctx, sdk.GetDataPlaneCertificatesSDK(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types - default: return fmt.Errorf("unsupported entity type %T", ent) } @@ -257,7 +257,8 @@ func Update[ return ctrl.Result{}, updateKeySet(ctx, sdk.GetKeySetsSDK(), ent) case *configurationv1alpha1.KongSNI: return ctrl.Result{}, updateSNI(ctx, sdk.GetSNIsSDK(), ent) - + case *configurationv1alpha1.KongDataPlaneClientCertificate: + return ctrl.Result{}, nil // DataPlaneCertificates are immutable. // --------------------------------------------------------------------- // TODO: add other Konnect types diff --git a/controller/konnect/ops/ops_kongdataplaneclientcertificate.go b/controller/konnect/ops/ops_kongdataplaneclientcertificate.go new file mode 100644 index 000000000..547ff796a --- /dev/null +++ b/controller/konnect/ops/ops_kongdataplaneclientcertificate.go @@ -0,0 +1,81 @@ +package ops + +import ( + "context" + "errors" + "fmt" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + 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" +) + +// createKongDataPlaneClientCertificate creates a KongDataPlaneClientCertificate in Konnect. +// It sets the KonnectID and the Programmed condition in the KongDataPlaneClientCertificate status. +func createKongDataPlaneClientCertificate( + ctx context.Context, + sdk DataPlaneClientCertificatesSDK, + cert *configurationv1alpha1.KongDataPlaneClientCertificate, +) error { + cpID := cert.GetControlPlaneID() + if cpID == "" { + return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", cert, client.ObjectKeyFromObject(cert)) + } + + resp, err := sdk.CreateDataplaneCertificate(ctx, + cpID, + &sdkkonnectcomp.DataPlaneClientCertificateRequest{ + Cert: cert.Spec.Cert, + }, + ) + + // 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, cert); errWrap != nil { + SetKonnectEntityProgrammedConditionFalse(cert, "FailedToCreate", errWrap.Error()) + return errWrap + } + + cert.Status.Konnect.SetKonnectID(*resp.DataPlaneClientCertificate.Item.ID) + SetKonnectEntityProgrammedCondition(cert) + + return nil +} + +// deleteKongDataPlaneClientCertificate deletes a KongDataPlaneClientCertificate in Konnect. +// The KongDataPlaneClientCertificate must have a KonnectID set in its status. +// It returns an error if the operation fails. +func deleteKongDataPlaneClientCertificate( + ctx context.Context, + sdk DataPlaneClientCertificatesSDK, + cert *configurationv1alpha1.KongDataPlaneClientCertificate, +) error { + id := cert.Status.Konnect.GetKonnectID() + _, err := sdk.DeleteDataplaneCertificate(ctx, cert.GetControlPlaneID(), id) + if errWrap := wrapErrIfKonnectOpFailed(err, DeleteOp, cert); errWrap != nil { + 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", cert.GetTypeName(), "id", id, + ) + return nil + } + return FailedKonnectOpError[configurationv1alpha1.KongDataPlaneClientCertificate]{ + Op: DeleteOp, + Err: sdkError, + } + } + return FailedKonnectOpError[configurationv1alpha1.KongDataPlaneClientCertificate]{ + Op: DeleteOp, + Err: errWrap, + } + } + + return nil +} diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index 33594fae2..c92be2d9c 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -26,6 +26,7 @@ type SDKWrapper interface { GetKeysSDK() KeysSDK GetKeySetsSDK() KeySetsSDK GetSNIsSDK() SNIsSDK + GetDataPlaneCertificatesSDK() DataPlaneClientCertificatesSDK } type sdkWrapper struct { @@ -129,6 +130,11 @@ func (w sdkWrapper) GetKeySetsSDK() KeySetsSDK { return w.sdk.KeySets } +// GetDataPlaneCertificatesSDK returns the SDK to operate data plane certificates. +func (w sdkWrapper) GetDataPlaneCertificatesSDK() DataPlaneClientCertificatesSDK { + return w.sdk.DPCertificates +} + // 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 873478b5c..250112e10 100644 --- a/controller/konnect/ops/sdkfactory_mock.go +++ b/controller/konnect/ops/sdkfactory_mock.go @@ -26,6 +26,7 @@ type MockSDKWrapper struct { KeysSDK *MockKeysSDK KeySetsSDK *MockKeySetsSDK SNIsSDK *MockSNIsSDK + DataPlaneCertificatesSDK *MockDataPlaneClientCertificatesSDK } var _ SDKWrapper = MockSDKWrapper{} @@ -51,6 +52,7 @@ func NewMockSDKWrapperWithT(t *testing.T) *MockSDKWrapper { KeysSDK: NewMockKeysSDK(t), KeySetsSDK: NewMockKeySetsSDK(t), SNIsSDK: NewMockSNIsSDK(t), + DataPlaneCertificatesSDK: NewMockDataPlaneClientCertificatesSDK(t), } } @@ -130,6 +132,10 @@ func (m MockSDKWrapper) GetSNIsSDK() SNIsSDK { return m.SNIsSDK } +func (m MockSDKWrapper) GetDataPlaneCertificatesSDK() DataPlaneClientCertificatesSDK { + return m.DataPlaneCertificatesSDK +} + type MockSDKFactory struct { t *testing.T SDK *MockSDKWrapper diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index 1e17d4199..db567905f 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -990,6 +990,11 @@ func getControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constrain return none } return mo.Some(*e.Spec.ControlPlaneRef) + case *configurationv1alpha1.KongDataPlaneClientCertificate: + if e.Spec.ControlPlaneRef == nil { + return none + } + return mo.Some(*e.Spec.ControlPlaneRef) default: return none } diff --git a/controller/konnect/watch.go b/controller/konnect/watch.go index 77f521c20..0442c6432 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -65,6 +65,8 @@ func ReconciliationWatchOptionsForEntity[ return KongKeySetReconciliationWatchOptions(cl) case *configurationv1alpha1.KongSNI: return KongSNIReconciliationWatchOptions(cl) + case *configurationv1alpha1.KongDataPlaneClientCertificate: + return KongDataPlaneClientCertificateReconciliationWatchOptions(cl) default: panic(fmt.Sprintf("unsupported entity type %T", ent)) } diff --git a/controller/konnect/watch_kongdataplanecertificate.go b/controller/konnect/watch_kongdataplanecertificate.go new file mode 100644 index 000000000..3ca320128 --- /dev/null +++ b/controller/konnect/watch_kongdataplanecertificate.go @@ -0,0 +1,145 @@ +package konnect + +import ( + "context" + "fmt" + + "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" + + "github.com/kong/gateway-operator/modules/manager/logging" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +// KongDataPlaneClientCertificateReconciliationWatchOptions returns the watch options for the KongDataPlaneClientCertificate. +func KongDataPlaneClientCertificateReconciliationWatchOptions(cl client.Client) []func(*ctrl.Builder) *ctrl.Builder { + return []func(*ctrl.Builder) *ctrl.Builder{ + func(b *ctrl.Builder) *ctrl.Builder { + return b.For(&configurationv1alpha1.KongDataPlaneClientCertificate{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(objRefersToKonnectGatewayControlPlane[configurationv1alpha1.KongDataPlaneClientCertificate]), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectAPIAuthConfiguration{}, + handler.EnqueueRequestsFromMapFunc( + enqueueKongDataPlaneClientCertificateForKonnectAPIAuthConfiguration(cl), + ), + ) + }, + func(b *ctrl.Builder) *ctrl.Builder { + return b.Watches( + &konnectv1alpha1.KonnectGatewayControlPlane{}, + handler.EnqueueRequestsFromMapFunc( + enqueueKongDataPlaneClientCertificateForKonnectControlPlane(cl), + ), + ) + }, + } +} + +func enqueueKongDataPlaneClientCertificateForKonnectControlPlane(cl client.Client) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + auth, ok := obj.(*konnectv1alpha1.KonnectAPIAuthConfiguration) + if !ok { + return nil + } + var l configurationv1alpha1.KongDataPlaneClientCertificateList + if err := cl.List(ctx, &l, + // TODO: change this when cross namespace refs are allowed. + client.InNamespace(auth.GetNamespace()), + client.MatchingFields{ + IndexFieldKongDataPlaneClientCertificateOnKonnectGatewayControlPlane: client.ObjectKeyFromObject(auth).String(), + }, + ); err != nil { + return nil + } + + return objectListToReconcileRequests(l.Items) + } +} + +func enqueueKongDataPlaneClientCertificateForKonnectAPIAuthConfiguration( + 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 configurationv1alpha1.KongDataPlaneClientCertificateList + if err := cl.List(ctx, &l, &client.ListOptions{ + // TODO: change this when cross namespace refs are allowed. + Namespace: auth.GetNamespace(), + }); err != nil { + return nil + } + + var ret []reconcile.Request + for _, dpCert := range l.Items { + cpRef, ok := getControlPlaneRef(&dpCert).Get() + if !ok { + continue + } + + switch cpRef.Type { + case configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef: + nn := types.NamespacedName{ + Name: cpRef.KonnectNamespacedRef.Name, + Namespace: dpCert.Namespace, + } + // TODO: change this when cross namespace refs are allowed. + if nn.Namespace != auth.Namespace { + continue + } + var cp konnectv1alpha1.KonnectGatewayControlPlane + if err := cl.Get(ctx, nn, &cp); err != nil { + ctrllog.FromContext(ctx).Error( + err, + "failed to get KonnectGatewayControlPlane", + "KonnectGatewayControlPlane", nn, + ) + continue + } + + // TODO: change this when cross namespace refs are allowed. + if cp.GetKonnectAPIAuthConfigurationRef().Name != auth.Name { + continue + } + + ret = append(ret, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Namespace: dpCert.Namespace, + Name: dpCert.Name, + }, + }) + + case configurationv1alpha1.ControlPlaneRefKonnectID: + ctrllog.FromContext(ctx).Error( + fmt.Errorf("unimplemented ControlPlaneRef type %q", cpRef.Type), + "unimplemented ControlPlaneRef for KongDataPlaneClientCertificate", + "KongDataPlaneClientCertificate", dpCert, "refType", cpRef.Type, + ) + continue + + default: + ctrllog.FromContext(ctx).V(logging.DebugLevel.Value()).Info( + "unsupported ControlPlaneRef for KongDataPlaneClientCertificate", + "KongDataPlaneClientCertificate", dpCert, "refType", cpRef.Type, + ) + continue + } + } + return ret + } +} diff --git a/go.mod b/go.mod index 133e75d03..9733221bb 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.24 + github.com/kong/kubernetes-configuration v0.0.27 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 0e65a6e12..29d56af5e 100644 --- a/go.sum +++ b/go.sum @@ -224,12 +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.24-0.20241001115459-c352e5229f8c h1:LLU0dCeln9PLdMkt6yTnx2Q9AZ62f7aas7LjyneIfrc= -github.com/kong/kubernetes-configuration v0.0.24-0.20241001115459-c352e5229f8c/go.mod h1:DXrWtdZzewUyPZBR4zvDoY/B8rHxeqcqCBDbyHA+B0Q= -github.com/kong/kubernetes-configuration v0.0.24-0.20241001135007-d03056a341ef h1:xrpbHFxoaHjeXmCS/9XnTzXZBN9nfuc9jLpOFEZB1nQ= -github.com/kong/kubernetes-configuration v0.0.24-0.20241001135007-d03056a341ef/go.mod h1:DXrWtdZzewUyPZBR4zvDoY/B8rHxeqcqCBDbyHA+B0Q= -github.com/kong/kubernetes-configuration v0.0.24 h1:XoZcJRgdPnzCDtmQ0y0PWVOkJoLAqkB0ODLEdhuc9og= -github.com/kong/kubernetes-configuration v0.0.24/go.mod h1:DXrWtdZzewUyPZBR4zvDoY/B8rHxeqcqCBDbyHA+B0Q= +github.com/kong/kubernetes-configuration v0.0.27 h1:4h1spllBnfF0P4+cr0J4xp+P5oGxAstgV8waCNkl5XI= +github.com/kong/kubernetes-configuration v0.0.27/go.mod h1:DXrWtdZzewUyPZBR4zvDoY/B8rHxeqcqCBDbyHA+B0Q= 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 7fdbe4e8a..3c9793c55 100644 --- a/modules/manager/controller_setup.go +++ b/modules/manager/controller_setup.go @@ -104,6 +104,8 @@ const ( KongKeySetControllerName = "KongKeySet" // KongSNIControllerName is the name of KongSNI controller. KongSNIControllerName = "KongSNI" + // KongDataPlaneClientCertificateControllerName is the name of KongDataPlaneClientCertificate controller. + KongDataPlaneClientCertificateControllerName = "KongDataPlaneClientCertificate" ) // SetupControllersShim runs SetupControllers and returns its result as a slice of the map values. @@ -513,6 +515,15 @@ func SetupControllers(mgr manager.Manager, c *Config) (map[string]ControllerDef, konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongKeySet](c.KonnectSyncPeriod), ), }, + KongDataPlaneClientCertificateControllerName: { + Enabled: c.KonnectControllersEnabled, + Controller: konnect.NewKonnectEntityReconciler[configurationv1alpha1.KongDataPlaneClientCertificate]( + sdkFactory, + c.DevelopmentMode, + mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongDataPlaneClientCertificate](c.KonnectSyncPeriod), + ), + }, KongPluginControllerName: { Enabled: c.KonnectControllersEnabled, Controller: konnect.NewKongPluginReconciler( @@ -624,6 +635,10 @@ func SetupCacheIndicesForKonnectTypes(ctx context.Context, mgr manager.Manager, Object: &configurationv1alpha1.KongKey{}, IndexOptions: konnect.IndexOptionsForKongKey(), }, + { + Object: &configurationv1alpha1.KongDataPlaneClientCertificate{}, + IndexOptions: konnect.IndexOptionsForKongDataPlaneCertificate(), + }, } for _, t := range types { diff --git a/test/envtest/konnect_entities_dataplaneclientcertificate_test.go b/test/envtest/konnect_entities_dataplaneclientcertificate_test.go new file mode 100644 index 000000000..b12897483 --- /dev/null +++ b/test/envtest/konnect_entities_dataplaneclientcertificate_test.go @@ -0,0 +1,101 @@ +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/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/conditions" + "github.com/kong/gateway-operator/controller/konnect/ops" + "github.com/kong/gateway-operator/modules/manager/scheme" + "github.com/kong/gateway-operator/test/helpers/deploy" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +func TestKongDataPlaneClientCertificate(t *testing.T) { + t.Parallel() + ctx, cancel := Context(t, context.Background()) + defer cancel() + cfg, ns := Setup(t, ctx, scheme.Get()) + + t.Log("Setting up the manager with reconcilers") + mgr, logs := NewManager(t, ctx, cfg, scheme.Get()) + factory := ops.NewMockSDKFactory(t) + sdk := factory.SDK + StartReconcilers(ctx, t, mgr, logs, + konnect.NewKonnectEntityReconciler(factory, false, mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongDataPlaneClientCertificate](konnectInfiniteSyncTime), + ), + ) + + t.Log("Setting up clients") + cl, err := client.NewWithWatch(mgr.GetConfig(), client.Options{ + Scheme: scheme.Get(), + }) + require.NoError(t, err) + clientNamespaced := client.NewNamespacedClient(mgr.GetClient(), ns.Name) + + t.Log("Creating KonnectAPIAuthConfiguration and KonnectGatewayControlPlane") + apiAuth := deploy.KonnectAPIAuthConfigurationWithProgrammed(t, ctx, clientNamespaced) + cp := deploy.KonnectGatewayControlPlaneWithID(t, ctx, clientNamespaced, apiAuth) + + t.Log("Setting up SDK expectations on KongDataPlaneClientCertificate creation") + const dpCertID = "dp-cert-id" + sdk.DataPlaneCertificatesSDK.EXPECT().CreateDataplaneCertificate(mock.Anything, cp.GetKonnectStatus().GetKonnectID(), + mock.MatchedBy(func(input *sdkkonnectcomp.DataPlaneClientCertificateRequest) bool { + return input.Cert == deploy.TestValidCACertPEM + }), + ).Return(&sdkkonnectops.CreateDataplaneCertificateResponse{ + DataPlaneClientCertificate: &sdkkonnectcomp.DataPlaneClientCertificate{ + Item: &sdkkonnectcomp.DataPlaneClientCertificateItem{ + ID: lo.ToPtr(dpCertID), + Cert: lo.ToPtr(deploy.TestValidCACertPEM), + }, + }, + }, nil) + + t.Log("Setting up a watch for KongDataPlaneClientCertificate events") + w := setupWatch[configurationv1alpha1.KongDataPlaneClientCertificateList](t, ctx, cl, client.InNamespace(ns.Name)) + + t.Log("Creating KongDataPlaneClientCertificate") + createdCert := deploy.KongDataPlaneClientCertificateAttachedToCP(t, ctx, clientNamespaced, cp) + + t.Log("Waiting for KongDataPlaneClientCertificate to be programmed") + watchFor(t, ctx, w, watch.Modified, func(c *configurationv1alpha1.KongDataPlaneClientCertificate) bool { + if c.GetName() != createdCert.GetName() { + return false + } + return lo.ContainsBy(c.Status.Conditions, func(condition metav1.Condition) bool { + return condition.Type == conditions.KonnectEntityProgrammedConditionType && + condition.Status == metav1.ConditionTrue + }) + }, "KongDataPlaneClientCertificate's Programmed condition should be true eventually") + + t.Log("Waiting for KongDataPlaneClientCertificate to be created in the SDK") + require.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.CACertificatesSDK.AssertExpectations(t)) + }, waitTime, tickTime) + + t.Log("Setting up SDK expectations on KongDataPlaneClientCertificate deletion") + sdk.DataPlaneCertificatesSDK.EXPECT().DeleteDataplaneCertificate(mock.Anything, cp.GetKonnectStatus().GetKonnectID(), dpCertID). + Return(&sdkkonnectops.DeleteDataplaneCertificateResponse{}, nil) + + t.Log("Deleting KongDataPlaneClientCertificate") + require.NoError(t, cl.Delete(ctx, createdCert)) + + t.Log("Waiting for KongDataPlaneClientCertificate to be deleted in the SDK") + assert.EventuallyWithT(t, func(c *assert.CollectT) { + assert.True(c, factory.SDK.CACertificatesSDK.AssertExpectations(t)) + }, waitTime, tickTime) +} diff --git a/test/helpers/deploy/deploy_resources.go b/test/helpers/deploy/deploy_resources.go index ee5cdb705..bcdbd9fdc 100644 --- a/test/helpers/deploy/deploy_resources.go +++ b/test/helpers/deploy/deploy_resources.go @@ -777,3 +777,34 @@ func KongSNIAttachedToCertificate( t.Logf("deployed KongSNI %s/%s", sni.Namespace, sni.Name) return sni } + +// KongDataPlaneClientCertificateAttachedToCP deploys a KongDataPlaneClientCertificate resource attached to a CP and returns the resource. +func KongDataPlaneClientCertificateAttachedToCP( + t *testing.T, + ctx context.Context, + cl client.Client, + cp *konnectv1alpha1.KonnectGatewayControlPlane, +) *configurationv1alpha1.KongDataPlaneClientCertificate { + t.Helper() + + cert := &configurationv1alpha1.KongDataPlaneClientCertificate{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "dp-cert-", + }, + Spec: configurationv1alpha1.KongDataPlaneClientCertificateSpec{ + ControlPlaneRef: &configurationv1alpha1.ControlPlaneRef{ + Type: configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef, + KonnectNamespacedRef: &configurationv1alpha1.KonnectNamespacedRef{ + Name: cp.GetName(), + }, + }, + KongDataPlaneClientCertificateAPISpec: configurationv1alpha1.KongDataPlaneClientCertificateAPISpec{ + Cert: TestValidCACertPEM, + }, + }, + } + require.NoError(t, cl.Create(ctx, cert)) + t.Logf("deployed new KongDataPlaneClientCertificate %s", client.ObjectKeyFromObject(cert)) + + return cert +}