diff --git a/.mockery.yaml b/.mockery.yaml index 26ddfbfc6..9dc0998ba 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -17,4 +17,5 @@ packages: ConsumersSDK: ConsumerGroupSDK: PluginSDK: + VaultSDK: MeSDK: diff --git a/config/rbac/role/role.yaml b/config/rbac/role/role.yaml index 9819d7dbe..8f1ea1492 100644 --- a/config/rbac/role/role.yaml +++ b/config/rbac/role/role.yaml @@ -133,7 +133,6 @@ rules: - kongingresses - konglicenses - kongupstreampolicies - - kongvaults - tcpingresses - udpingresses verbs: @@ -167,6 +166,7 @@ rules: - kongpluginbindings - kongroutes - kongservices + - kongvaults verbs: - get - list diff --git a/controller/konnect/constraints/constraints.go b/controller/konnect/constraints/constraints.go index d3775a688..8f472c7bb 100644 --- a/controller/konnect/constraints/constraints.go +++ b/controller/konnect/constraints/constraints.go @@ -18,7 +18,8 @@ type SupportedKonnectEntityType interface { configurationv1alpha1.KongRoute | configurationv1.KongConsumer | configurationv1beta1.KongConsumerGroup | - configurationv1alpha1.KongPluginBinding + configurationv1alpha1.KongPluginBinding | + configurationv1alpha1.KongVault // TODO: add other types GetTypeName() string diff --git a/controller/konnect/ops/kongvault.go b/controller/konnect/ops/kongvault.go new file mode 100644 index 000000000..848780a04 --- /dev/null +++ b/controller/konnect/ops/kongvault.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// VaultSDK is the interface for Konnect Vault SDK. +type VaultSDK interface { + CreateVault(ctx context.Context, controlPlaneID string, vault sdkkonnectcomp.VaultInput, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateVaultResponse, error) + UpsertVault(ctx context.Context, request sdkkonnectops.UpsertVaultRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertVaultResponse, error) + DeleteVault(ctx context.Context, controlPlaneID string, vaultID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteVaultResponse, error) +} diff --git a/controller/konnect/ops/kongvault_mock.go b/controller/konnect/ops/kongvault_mock.go new file mode 100644 index 000000000..6dbb84a75 --- /dev/null +++ b/controller/konnect/ops/kongvault_mock.go @@ -0,0 +1,264 @@ +// 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" +) + +// MockVaultSDK is an autogenerated mock type for the VaultSDK type +type MockVaultSDK struct { + mock.Mock +} + +type MockVaultSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockVaultSDK) EXPECT() *MockVaultSDK_Expecter { + return &MockVaultSDK_Expecter{mock: &_m.Mock} +} + +// CreateVault provides a mock function with given fields: ctx, controlPlaneID, vault, opts +func (_m *MockVaultSDK) CreateVault(ctx context.Context, controlPlaneID string, vault components.VaultInput, opts ...operations.Option) (*operations.CreateVaultResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, vault) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateVault") + } + + var r0 *operations.CreateVaultResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, components.VaultInput, ...operations.Option) (*operations.CreateVaultResponse, error)); ok { + return rf(ctx, controlPlaneID, vault, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, components.VaultInput, ...operations.Option) *operations.CreateVaultResponse); ok { + r0 = rf(ctx, controlPlaneID, vault, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateVaultResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, components.VaultInput, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, vault, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockVaultSDK_CreateVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateVault' +type MockVaultSDK_CreateVault_Call struct { + *mock.Call +} + +// CreateVault is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - vault components.VaultInput +// - opts ...operations.Option +func (_e *MockVaultSDK_Expecter) CreateVault(ctx interface{}, controlPlaneID interface{}, vault interface{}, opts ...interface{}) *MockVaultSDK_CreateVault_Call { + return &MockVaultSDK_CreateVault_Call{Call: _e.mock.On("CreateVault", + append([]interface{}{ctx, controlPlaneID, vault}, opts...)...)} +} + +func (_c *MockVaultSDK_CreateVault_Call) Run(run func(ctx context.Context, controlPlaneID string, vault components.VaultInput, opts ...operations.Option)) *MockVaultSDK_CreateVault_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.VaultInput), variadicArgs...) + }) + return _c +} + +func (_c *MockVaultSDK_CreateVault_Call) Return(_a0 *operations.CreateVaultResponse, _a1 error) *MockVaultSDK_CreateVault_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockVaultSDK_CreateVault_Call) RunAndReturn(run func(context.Context, string, components.VaultInput, ...operations.Option) (*operations.CreateVaultResponse, error)) *MockVaultSDK_CreateVault_Call { + _c.Call.Return(run) + return _c +} + +// DeleteVault provides a mock function with given fields: ctx, controlPlaneID, vaultID, opts +func (_m *MockVaultSDK) DeleteVault(ctx context.Context, controlPlaneID string, vaultID string, opts ...operations.Option) (*operations.DeleteVaultResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, vaultID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteVault") + } + + var r0 *operations.DeleteVaultResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) (*operations.DeleteVaultResponse, error)); ok { + return rf(ctx, controlPlaneID, vaultID, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) *operations.DeleteVaultResponse); ok { + r0 = rf(ctx, controlPlaneID, vaultID, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteVaultResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, vaultID, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockVaultSDK_DeleteVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteVault' +type MockVaultSDK_DeleteVault_Call struct { + *mock.Call +} + +// DeleteVault is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - vaultID string +// - opts ...operations.Option +func (_e *MockVaultSDK_Expecter) DeleteVault(ctx interface{}, controlPlaneID interface{}, vaultID interface{}, opts ...interface{}) *MockVaultSDK_DeleteVault_Call { + return &MockVaultSDK_DeleteVault_Call{Call: _e.mock.On("DeleteVault", + append([]interface{}{ctx, controlPlaneID, vaultID}, opts...)...)} +} + +func (_c *MockVaultSDK_DeleteVault_Call) Run(run func(ctx context.Context, controlPlaneID string, vaultID string, opts ...operations.Option)) *MockVaultSDK_DeleteVault_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 *MockVaultSDK_DeleteVault_Call) Return(_a0 *operations.DeleteVaultResponse, _a1 error) *MockVaultSDK_DeleteVault_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockVaultSDK_DeleteVault_Call) RunAndReturn(run func(context.Context, string, string, ...operations.Option) (*operations.DeleteVaultResponse, error)) *MockVaultSDK_DeleteVault_Call { + _c.Call.Return(run) + return _c +} + +// UpsertVault provides a mock function with given fields: ctx, request, opts +func (_m *MockVaultSDK) UpsertVault(ctx context.Context, request operations.UpsertVaultRequest, opts ...operations.Option) (*operations.UpsertVaultResponse, 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 UpsertVault") + } + + var r0 *operations.UpsertVaultResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertVaultRequest, ...operations.Option) (*operations.UpsertVaultResponse, error)); ok { + return rf(ctx, request, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertVaultRequest, ...operations.Option) *operations.UpsertVaultResponse); ok { + r0 = rf(ctx, request, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpsertVaultResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.UpsertVaultRequest, ...operations.Option) error); ok { + r1 = rf(ctx, request, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockVaultSDK_UpsertVault_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertVault' +type MockVaultSDK_UpsertVault_Call struct { + *mock.Call +} + +// UpsertVault is a helper method to define mock.On call +// - ctx context.Context +// - request operations.UpsertVaultRequest +// - opts ...operations.Option +func (_e *MockVaultSDK_Expecter) UpsertVault(ctx interface{}, request interface{}, opts ...interface{}) *MockVaultSDK_UpsertVault_Call { + return &MockVaultSDK_UpsertVault_Call{Call: _e.mock.On("UpsertVault", + append([]interface{}{ctx, request}, opts...)...)} +} + +func (_c *MockVaultSDK_UpsertVault_Call) Run(run func(ctx context.Context, request operations.UpsertVaultRequest, opts ...operations.Option)) *MockVaultSDK_UpsertVault_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.UpsertVaultRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockVaultSDK_UpsertVault_Call) Return(_a0 *operations.UpsertVaultResponse, _a1 error) *MockVaultSDK_UpsertVault_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockVaultSDK_UpsertVault_Call) RunAndReturn(run func(context.Context, operations.UpsertVaultRequest, ...operations.Option) (*operations.UpsertVaultResponse, error)) *MockVaultSDK_UpsertVault_Call { + _c.Call.Return(run) + return _c +} + +// NewMockVaultSDK creates a new instance of MockVaultSDK. 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 NewMockVaultSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockVaultSDK { + mock := &MockVaultSDK{} + 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 94c851aaa..3a7f78c8c 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -64,6 +64,8 @@ func Create[ return e, createConsumerGroup(ctx, sdk.GetConsumerGroupsSDK(), ent) case *configurationv1alpha1.KongPluginBinding: return e, createPlugin(ctx, cl, sdk.GetPluginSDK(), ent) + case *configurationv1alpha1.KongVault: + return e, createVault(ctx, sdk.GetVaultSDK(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types @@ -102,6 +104,8 @@ func Delete[ return deleteConsumerGroup(ctx, sdk.GetConsumerGroupsSDK(), ent) case *configurationv1alpha1.KongPluginBinding: return deletePlugin(ctx, sdk.GetPluginSDK(), ent) + case *configurationv1alpha1.KongVault: + return deleteVault(ctx, sdk.GetVaultSDK(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types @@ -165,6 +169,8 @@ func Update[ return ctrl.Result{}, updateConsumerGroup(ctx, sdk.GetConsumerGroupsSDK(), ent) case *configurationv1alpha1.KongPluginBinding: return ctrl.Result{}, updatePlugin(ctx, sdk.GetPluginSDK(), cl, ent) + case *configurationv1alpha1.KongVault: + return ctrl.Result{}, updateVault(ctx, sdk.GetVaultSDK(), ent) // --------------------------------------------------------------------- // TODO: add other Konnect types diff --git a/controller/konnect/ops/ops_kongvault.go b/controller/konnect/ops/ops_kongvault.go new file mode 100644 index 000000000..d242d6f8b --- /dev/null +++ b/controller/konnect/ops/ops_kongvault.go @@ -0,0 +1,192 @@ +package ops + +import ( + "context" + "errors" + "fmt" + "net/http" + "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" +) + +// TODO: use vault.GetControlPlaneID() after https://github.com/Kong/kubernetes-configuration/pull/77 merged +func getKongVaultControlPlaneID(vault *configurationv1alpha1.KongVault) string { + if vault.Status.Konnect == nil { + return "" + } + return vault.Status.Konnect.ID +} + +func createVault(ctx context.Context, sdk VaultSDK, vault *configurationv1alpha1.KongVault) error { + cpID := getKongVaultControlPlaneID(vault) + if cpID == "" { + return fmt.Errorf( + "can't create %T %s without a Konnect ControlPlane ID", + vault, client.ObjectKeyFromObject(vault), + ) + } + resp, err := sdk.CreateVault(ctx, cpID, kongVaultToVaultInput(vault)) + + if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, vault); errWrapped != nil { + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionFalse, + "FailedToCreate", + errWrapped.Error(), + vault.GetGeneration(), + ), + vault, + ) + return errWrapped + } + + vault.Status.Konnect.SetKonnectID(*resp.Vault.ID) + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionTrue, + conditions.KonnectEntityProgrammedReasonProgrammed, + "", + vault.GetGeneration(), + ), + vault, + ) + + return nil +} + +func updateVault(ctx context.Context, sdk VaultSDK, vault *configurationv1alpha1.KongVault) error { + cpID := getKongVaultControlPlaneID(vault) + if cpID == "" { + return fmt.Errorf( + "can't update %T %s without a Konnect ControlPlane ID", + vault, client.ObjectKeyFromObject(vault), + ) + } + id := vault.GetKonnectID() + _, err := sdk.UpsertVault(ctx, sdkkonnectops.UpsertVaultRequest{ + VaultID: id, + ControlPlaneID: cpID, + Vault: kongVaultToVaultInput(vault), + }) + + if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, vault); errWrapped != nil { + // Service update operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrapped, &sdkError) { + switch sdkError.StatusCode { + // REVIEW: should we use constants defined in `net/http` or numerics here? + case http.StatusNotFound: + if err := createVault(ctx, sdk, vault); err != nil { + return FailedKonnectOpError[configurationv1alpha1.KongVault]{ + Op: UpdateOp, + Err: err, + } + } + // Create succeeded, createVault sets the status so no need to do this here. + return nil + default: + return FailedKonnectOpError[configurationv1alpha1.KongVault]{ + Op: UpdateOp, + Err: sdkError, + } + } + } + + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionFalse, + "FailedToUpdate", + errWrapped.Error(), + vault.GetGeneration(), + ), + vault, + ) + return errWrapped + } + + k8sutils.SetCondition( + k8sutils.NewConditionWithGeneration( + conditions.KonnectEntityProgrammedConditionType, + metav1.ConditionTrue, + conditions.KonnectEntityProgrammedReasonProgrammed, + "", + vault.GetGeneration(), + ), + vault, + ) + + return nil +} + +func deleteVault(ctx context.Context, sdk VaultSDK, vault *configurationv1alpha1.KongVault) error { + cpID := getKongVaultControlPlaneID(vault) + if cpID == "" { + return fmt.Errorf( + "can't delete %T %s without a Konnect ControlPlane ID", + vault, client.ObjectKeyFromObject(vault), + ) + } + id := vault.GetKonnectStatus().GetKonnectID() + _, err := sdk.DeleteVault(ctx, cpID, id) + if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, vault); errWrapped != nil { + // Vault delete operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrapped, &sdkError) { + switch sdkError.StatusCode { + case http.StatusNotFound: + ctrllog.FromContext(ctx). + Info("entity not found in Konnect, skipping delete", + "op", DeleteOp, "type", vault.GetTypeName(), "id", id, + ) + return nil + default: + return FailedKonnectOpError[configurationv1alpha1.KongVault]{ + Op: DeleteOp, + Err: sdkError, + } + } + } + return FailedKonnectOpError[configurationv1alpha1.KongVault]{ + Op: DeleteOp, + Err: errWrapped, + } + } + + return nil +} + +func kongVaultToVaultInput(vault *configurationv1alpha1.KongVault) sdkkonnectcomp.VaultInput { + var ( + specTags = vault.Spec.Tags + annotationTags = metadata.ExtractTags(vault) + k8sTags = GenerateKubernetesMetadataTags(vault) + ) + // Deduplicate tags to avoid rejection by Konnect. + tags := lo.Uniq(slices.Concat(specTags, annotationTags, k8sTags)) + input := sdkkonnectcomp.VaultInput{ + Config: &sdkkonnectcomp.VaultConfig{}, + Name: lo.ToPtr(vault.Spec.Backend), + Prefix: lo.ToPtr(vault.Spec.Prefix), + Tags: tags, + } + if vault.Spec.Description != "" { + input.Description = lo.ToPtr(vault.Spec.Description) + } + return input +} diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index 35923aa26..11556ebb3 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -13,6 +13,7 @@ type SDKWrapper interface { GetConsumersSDK() ConsumersSDK GetConsumerGroupsSDK() ConsumerGroupSDK GetPluginSDK() PluginSDK + GetVaultSDK() VaultSDK GetMeSDK() MeSDK } @@ -52,6 +53,11 @@ func (w sdkWrapper) GetPluginSDK() PluginSDK { return w.sdk.Plugins } +// GetVaultSDK returns the SDK to operate Vaults. +func (w sdkWrapper) GetVaultSDK() VaultSDK { + return w.sdk.Vaults +} + // GetMeSDK returns the "me" SDK to get current organization. func (w sdkWrapper) GetMeSDK() MeSDK { return w.sdk.Me diff --git a/controller/konnect/ops/sdkfactory_mock.go b/controller/konnect/ops/sdkfactory_mock.go index 3bfd47fa9..94d3a6889 100644 --- a/controller/konnect/ops/sdkfactory_mock.go +++ b/controller/konnect/ops/sdkfactory_mock.go @@ -13,6 +13,7 @@ type MockSDKWrapper struct { ConsumersSDK *MockConsumersSDK ConsumerGroupSDK *MockConsumerGroupSDK PluginSDK *MockPluginSDK + VaultSDK *MockVaultSDK MeSDK *MockMeSDK } @@ -26,6 +27,7 @@ func NewMockSDKWrapperWithT(t *testing.T) *MockSDKWrapper { ConsumersSDK: NewMockConsumersSDK(t), ConsumerGroupSDK: NewMockConsumerGroupSDK(t), PluginSDK: NewMockPluginSDK(t), + VaultSDK: NewMockVaultSDK(t), MeSDK: NewMockMeSDK(t), } } @@ -54,6 +56,10 @@ func (m MockSDKWrapper) GetPluginSDK() PluginSDK { return m.PluginSDK } +func (m MockSDKWrapper) GetVaultSDK() VaultSDK { + return m.VaultSDK +} + func (m MockSDKWrapper) GetMeSDK() MeSDK { return m.MeSDK } diff --git a/controller/konnect/reconciler_generic_rbac.go b/controller/konnect/reconciler_generic_rbac.go index e71951562..7d0cdf80b 100644 --- a/controller/konnect/reconciler_generic_rbac.go +++ b/controller/konnect/reconciler_generic_rbac.go @@ -15,4 +15,7 @@ package konnect //+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes,verbs=get;list;watch;update;patch //+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongroutes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults,verbs=get;list;watch;update;patch +//+kubebuilder:rbac:groups=configuration.konghq.com,resources=kongvaults/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 4b9001f70..4c6ed30de 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -36,6 +36,8 @@ func ReconciliationWatchOptionsForEntity[ return KonnectGatewayControlPlaneReconciliationWatchOptions(cl) case *configurationv1alpha1.KongPluginBinding: return KongPluginBindingReconciliationWatchOptions(cl) + case *configurationv1alpha1.KongVault: + return KongVaultReconciliationWatchOptions(cl) default: panic(fmt.Sprintf("unsupported entity type %T", ent)) } diff --git a/controller/konnect/watch_kongvault.go b/controller/konnect/watch_kongvault.go new file mode 100644 index 000000000..c72f16510 --- /dev/null +++ b/controller/konnect/watch_kongvault.go @@ -0,0 +1,46 @@ +package konnect + +import ( + "context" + "reflect" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + operatorerrors "github.com/kong/gateway-operator/internal/errors" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" +) + +// KongVaultReconciliationWatchOptions returns the watch options for KongVault. +func KongVaultReconciliationWatchOptions(cl client.Client) []func(*ctrl.Builder) *ctrl.Builder { + return []func(*ctrl.Builder) *ctrl.Builder{ + func(b *ctrl.Builder) *ctrl.Builder { + return b.For(&configurationv1alpha1.KongVault{}, + builder.WithPredicates( + predicate.NewPredicateFuncs(kongVaultRefersToKonnectGatewayControlPlane()), + ), + ) + }, + } +} + +func kongVaultRefersToKonnectGatewayControlPlane() func(obj client.Object) bool { + return func(obj client.Object) bool { + kongVault, ok := obj.(*configurationv1alpha1.KongVault) + if !ok { + ctrllog.FromContext(context.Background()).Error( + operatorerrors.ErrUnexpectedObject, + "failed to run predicate function", + "expected", "KongVault", "found", reflect.TypeOf(obj), + ) + return false + } + + cpRef := kongVault.Spec.ControlPlaneRef + return cpRef != nil && cpRef.Type == configurationv1alpha1.ControlPlaneRefKonnectNamespacedRef + } +} diff --git a/go.mod b/go.mod index 3847f8163..1dd7f01dc 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.12 + github.com/kong/kubernetes-configuration v0.0.13 github.com/kong/kubernetes-ingress-controller/v3 v3.3.1 github.com/kong/kubernetes-telemetry v0.1.5 github.com/kong/kubernetes-testing-framework v0.47.2 @@ -193,7 +193,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiextensions-apiserver v0.31.0 + k8s.io/apiextensions-apiserver v0.31.1 k8s.io/component-base v0.31.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20240430033511-f0e62f92d13f // indirect diff --git a/go.sum b/go.sum index 27db88c91..1a185bcdb 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.0 h1:U6dE2sqb8E8j0kESW/RCW9TkXH8Y3W0EtNDXJVsDNuM= github.com/kong/go-kong v0.59.0/go.mod h1:8Vt6HmtgLNgL/7bSwAlz3DIWqBtzG7qEt9+OnMiQOa0= -github.com/kong/kubernetes-configuration v0.0.12 h1:5Q4OxfNgl68CRBrYo/mv/6inutEG4S9tzLk2xjsKIrU= -github.com/kong/kubernetes-configuration v0.0.12/go.mod h1:Zs8Fd8ZA4+xOjM7HSV6bL0Fr6dwL0exddWN1qRWQH3U= +github.com/kong/kubernetes-configuration v0.0.13 h1:YuknRM1L/OiXAqq/HIBNfBSxjC+Pi0N+vepCIG+bSCM= +github.com/kong/kubernetes-configuration v0.0.13/go.mod h1:nB2ilAgUc80qus6tICn3NCk2je9QAy0iHofG2Itx2hY= github.com/kong/kubernetes-ingress-controller/v3 v3.3.1 h1:uWlcwz5oAnVyUZdtDV9p2l9CdlHhLNTKey3AcHF/Jxs= github.com/kong/kubernetes-ingress-controller/v3 v3.3.1/go.mod h1:2CBAJ7/J+FyAFn7Y8OLoTO3ApM+qiGIgNLbCyy98Vqk= github.com/kong/kubernetes-telemetry v0.1.5 h1:xHwU1q0IvfEYqpj03po73ZKbVarnFPUwzkoFkdVnr9w= diff --git a/modules/manager/controller_setup.go b/modules/manager/controller_setup.go index 8e186a306..9162dad08 100644 --- a/modules/manager/controller_setup.go +++ b/modules/manager/controller_setup.go @@ -72,6 +72,8 @@ const ( KongConsumerGroupControllerName = "KongConsumerGroup" // KongPluginBindingControllerName is the name of the KongPluginBinding controller. KongPluginBindingControllerName = "KongPluginBinding" + // KongVaultContollerName is the name of KongVault controller. + KongVaultControllerName = "KongVault" ) // SetupControllersShim runs SetupControllers and returns its result as a slice of the map values. @@ -372,6 +374,15 @@ func SetupControllers(mgr manager.Manager, c *Config) (map[string]ControllerDef, konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongPluginBinding](c.KonnectSyncPeriod), ), }, + KongVaultControllerName: { + Enabled: c.KonnectControllersEnabled, + Controller: konnect.NewKonnectEntityReconciler[configurationv1alpha1.KongVault]( + sdkFactory, + c.DevelopmentMode, + mgr.GetClient(), + konnect.WithKonnectEntitySyncPeriod[configurationv1alpha1.KongVault](c.KonnectSyncPeriod), + ), + }, } // Merge Konnect controllers into the controllers map. This is done this way instead of directly assigning diff --git a/test/envtest/reconciler_setupwithmanager_test.go b/test/envtest/reconciler_setupwithmanager_test.go index 025dca1e1..7c542850d 100644 --- a/test/envtest/reconciler_setupwithmanager_test.go +++ b/test/envtest/reconciler_setupwithmanager_test.go @@ -41,6 +41,7 @@ func TestNewKonnectEntityReconciler(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.KongVault{}, nil) } type konnectEntityReconcilerTestCase struct {