From 17311efd12702c64bad15ab9899131cc3f02df08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Mon, 7 Oct 2024 13:16:16 +0200 Subject: [PATCH] feat(konnect): allow setting ControlPlane group members (#697) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(konnect): allow setting ControlPlane group members * chore: reorder variable declaration * Update controller/konnect/ops/ops_controlplane.go Co-authored-by: Grzegorz Burzyński * refactor: update status after Konnect update * refactor: use sourcegraph/conc for concurrency --------- Co-authored-by: Grzegorz Burzyński --- .mockery.yaml | 1 + CHANGELOG.md | 2 + ..._gatewaycontrolplane_group_assignment.yaml | 61 ++++ controller/konnect/conditions/conditions.go | 5 + controller/konnect/ops/controlplane.go | 2 +- controller/konnect/ops/controlplanegroup.go | 13 + .../konnect/ops/controlplanegroup_mock.go | 115 +++++++ controller/konnect/ops/ops.go | 4 +- controller/konnect/ops/ops_controlplane.go | 83 ++++- .../konnect/ops/ops_controlplane_test.go | 325 +++++++++++++++++- controller/konnect/ops/sdkfactory.go | 8 +- controller/konnect/ops/sdkfactory_mock.go | 6 + controller/konnect/reconciler_generic.go | 12 +- docs/api-reference.md | 1 + go.mod | 3 +- go.sum | 6 +- ...nnect_entities_gatewaycontrolplane_test.go | 215 +++++++++--- test/envtest/konnect_entities_suite_test.go | 14 +- test/helpers/deploy/deploy_resources.go | 3 +- 19 files changed, 788 insertions(+), 91 deletions(-) create mode 100644 config/samples/konnect_gatewaycontrolplane_group_assignment.yaml create mode 100644 controller/konnect/ops/controlplanegroup.go create mode 100644 controller/konnect/ops/controlplanegroup_mock.go diff --git a/.mockery.yaml b/.mockery.yaml index 662d84fd6..fb370c56b 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -12,6 +12,7 @@ packages: github.com/kong/gateway-operator/controller/konnect/ops: interfaces: ControlPlaneSDK: + ControlPlaneGroupSDK: ServicesSDK: RoutesSDK: ConsumersSDK: diff --git a/CHANGELOG.md b/CHANGELOG.md index e425d24a5..6d408b03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -99,6 +99,8 @@ - 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) +- Allow setting `KonnectGatewayControlPlane`s group membership + [#697](https://github.com/Kong/gateway-operator/pull/697) ### Fixed diff --git a/config/samples/konnect_gatewaycontrolplane_group_assignment.yaml b/config/samples/konnect_gatewaycontrolplane_group_assignment.yaml new file mode 100644 index 000000000..41570138b --- /dev/null +++ b/config/samples/konnect_gatewaycontrolplane_group_assignment.yaml @@ -0,0 +1,61 @@ +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 + konnect: + authRef: + name: konnect-api-auth-dev-1 +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: test2 + namespace: default +spec: + name: test2 + konnect: + authRef: + name: konnect-api-auth-dev-1 +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: test3 + namespace: default +spec: + name: test3 + konnect: + authRef: + name: konnect-api-auth-dev-1 +--- +kind: KonnectGatewayControlPlane +apiVersion: konnect.konghq.com/v1alpha1 +metadata: + name: group1 + namespace: default +spec: + name: group1 + cluster_type: CLUSTER_TYPE_CONTROL_PLANE_GROUP + members: + - name: test1 + - name: test2 + - name: test3 + labels: + app: group1 + key1: group1 + konnect: + authRef: + name: konnect-api-auth-dev-1 diff --git a/controller/konnect/conditions/conditions.go b/controller/konnect/conditions/conditions.go index 532809f09..deaab9c9a 100644 --- a/controller/konnect/conditions/conditions.go +++ b/controller/konnect/conditions/conditions.go @@ -20,6 +20,11 @@ const ( // KonnectEntityProgrammedReasonFailedToReconcileConsumerGroupsWithKonnect is the reason for the Programmed condition. // It is set when one or more KongConsumerGroup references could not be reconciled with Konnect. KonnectEntityProgrammedReasonFailedToReconcileConsumerGroupsWithKonnect = "FailedToReconcileConsumerGroupsWithKonnect" + + // KonnectGatewayControlPlaneProgrammedReasonFailedToSetControlPlaneGroupMembers + // is the reason for the Programmed condition. It is set when the control plane + // group members could not be set. + KonnectGatewayControlPlaneProgrammedReasonFailedToSetControlPlaneGroupMembers = "FailedToSetControlPlaneGroupMembers" ) const ( diff --git a/controller/konnect/ops/controlplane.go b/controller/konnect/ops/controlplane.go index 4da05c1bd..9ea41f2df 100644 --- a/controller/konnect/ops/controlplane.go +++ b/controller/konnect/ops/controlplane.go @@ -7,7 +7,7 @@ import ( sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" ) -// ControlPlaneSDK is the interface for the Konnect ControlPlaneSDK SDK. +// ControlPlaneSDK is the interface for the Konnect ControlPlane SDK. type ControlPlaneSDK interface { CreateControlPlane(ctx context.Context, req sdkkonnectcomp.CreateControlPlaneRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateControlPlaneResponse, error) DeleteControlPlane(ctx context.Context, id string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteControlPlaneResponse, error) diff --git a/controller/konnect/ops/controlplanegroup.go b/controller/konnect/ops/controlplanegroup.go new file mode 100644 index 000000000..ff125f47b --- /dev/null +++ b/controller/konnect/ops/controlplanegroup.go @@ -0,0 +1,13 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// ControlPlaneGroupSDK is the interface for the Konnect ControlPlaneGroupSDK SDK. +type ControlPlaneGroupSDK interface { + PutControlPlanesIDGroupMemberships(ctx context.Context, id string, groupMembership *sdkkonnectcomp.GroupMembership, opts ...sdkkonnectops.Option) (*sdkkonnectops.PutControlPlanesIDGroupMembershipsResponse, error) +} diff --git a/controller/konnect/ops/controlplanegroup_mock.go b/controller/konnect/ops/controlplanegroup_mock.go new file mode 100644 index 000000000..3fdcc4d64 --- /dev/null +++ b/controller/konnect/ops/controlplanegroup_mock.go @@ -0,0 +1,115 @@ +// 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" +) + +// MockControlPlaneGroupSDK is an autogenerated mock type for the ControlPlaneGroupSDK type +type MockControlPlaneGroupSDK struct { + mock.Mock +} + +type MockControlPlaneGroupSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockControlPlaneGroupSDK) EXPECT() *MockControlPlaneGroupSDK_Expecter { + return &MockControlPlaneGroupSDK_Expecter{mock: &_m.Mock} +} + +// PutControlPlanesIDGroupMemberships provides a mock function with given fields: ctx, id, groupMembership, opts +func (_m *MockControlPlaneGroupSDK) PutControlPlanesIDGroupMemberships(ctx context.Context, id string, groupMembership *components.GroupMembership, opts ...operations.Option) (*operations.PutControlPlanesIDGroupMembershipsResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id, groupMembership) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for PutControlPlanesIDGroupMemberships") + } + + var r0 *operations.PutControlPlanesIDGroupMembershipsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *components.GroupMembership, ...operations.Option) (*operations.PutControlPlanesIDGroupMembershipsResponse, error)); ok { + return rf(ctx, id, groupMembership, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *components.GroupMembership, ...operations.Option) *operations.PutControlPlanesIDGroupMembershipsResponse); ok { + r0 = rf(ctx, id, groupMembership, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.PutControlPlanesIDGroupMembershipsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *components.GroupMembership, ...operations.Option) error); ok { + r1 = rf(ctx, id, groupMembership, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutControlPlanesIDGroupMemberships' +type MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call struct { + *mock.Call +} + +// PutControlPlanesIDGroupMemberships is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - groupMembership *components.GroupMembership +// - opts ...operations.Option +func (_e *MockControlPlaneGroupSDK_Expecter) PutControlPlanesIDGroupMemberships(ctx interface{}, id interface{}, groupMembership interface{}, opts ...interface{}) *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call { + return &MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call{Call: _e.mock.On("PutControlPlanesIDGroupMemberships", + append([]interface{}{ctx, id, groupMembership}, opts...)...)} +} + +func (_c *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call) Run(run func(ctx context.Context, id string, groupMembership *components.GroupMembership, opts ...operations.Option)) *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_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.GroupMembership), variadicArgs...) + }) + return _c +} + +func (_c *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call) Return(_a0 *operations.PutControlPlanesIDGroupMembershipsResponse, _a1 error) *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call) RunAndReturn(run func(context.Context, string, *components.GroupMembership, ...operations.Option) (*operations.PutControlPlanesIDGroupMembershipsResponse, error)) *MockControlPlaneGroupSDK_PutControlPlanesIDGroupMemberships_Call { + _c.Call.Return(run) + return _c +} + +// NewMockControlPlaneGroupSDK creates a new instance of MockControlPlaneGroupSDK. 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 NewMockControlPlaneGroupSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockControlPlaneGroupSDK { + mock := &MockControlPlaneGroupSDK{} + 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 26afbf7f0..c9238e776 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -53,7 +53,7 @@ func Create[ switch ent := any(e).(type) { case *konnectv1alpha1.KonnectGatewayControlPlane: - return e, createControlPlane(ctx, sdk.GetControlPlaneSDK(), ent) + return e, createControlPlane(ctx, sdk.GetControlPlaneSDK(), sdk.GetControlPlaneGroupSDK(), cl, ent) case *configurationv1alpha1.KongService: return e, createService(ctx, sdk.GetServicesSDK(), ent) case *configurationv1alpha1.KongRoute: @@ -226,7 +226,7 @@ func Update[ switch ent := any(e).(type) { case *konnectv1alpha1.KonnectGatewayControlPlane: - return ctrl.Result{}, updateControlPlane(ctx, sdk.GetControlPlaneSDK(), ent) + return ctrl.Result{}, updateControlPlane(ctx, sdk.GetControlPlaneSDK(), sdk.GetControlPlaneGroupSDK(), cl, ent) case *configurationv1alpha1.KongService: return ctrl.Result{}, updateService(ctx, sdk.GetServicesSDK(), ent) case *configurationv1alpha1.KongRoute: diff --git a/controller/konnect/ops/ops_controlplane.go b/controller/konnect/ops/ops_controlplane.go index 1905cd5f5..2c9bd7fa0 100644 --- a/controller/konnect/ops/ops_controlplane.go +++ b/controller/konnect/ops/ops_controlplane.go @@ -3,18 +3,28 @@ package ops import ( "context" "errors" + "fmt" + "sort" sdkkonnectgo "github.com/Kong/sdk-konnect-go" sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" + "github.com/samber/lo" + "github.com/sourcegraph/conc/iter" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) func createControlPlane( ctx context.Context, sdk ControlPlaneSDK, + sdkGroups ControlPlaneGroupSDK, + cl client.Client, cp *konnectv1alpha1.KonnectGatewayControlPlane, ) error { req := cp.Spec.CreateControlPlaneRequest @@ -30,6 +40,12 @@ func createControlPlane( } cp.Status.SetKonnectID(*resp.ControlPlane.ID) + + if err := setGroupMembers(ctx, cl, cp, sdkGroups); err != nil { + SetKonnectEntityProgrammedConditionFalse(cp, conditions.KonnectGatewayControlPlaneProgrammedReasonFailedToSetControlPlaneGroupMembers, err.Error()) + return err + } + SetKonnectEntityProgrammedCondition(cp) return nil @@ -75,6 +91,8 @@ func deleteControlPlane( func updateControlPlane( ctx context.Context, sdk ControlPlaneSDK, + sdkGroups ControlPlaneGroupSDK, + cl client.Client, cp *konnectv1alpha1.KonnectGatewayControlPlane, ) error { id := cp.GetKonnectStatus().GetKonnectID() @@ -93,7 +111,7 @@ func updateControlPlane( Info("entity not found in Konnect, trying to recreate", "type", cp.GetTypeName(), "id", id, ) - if err := createControlPlane(ctx, sdk, cp); err != nil { + if err := createControlPlane(ctx, sdk, sdkGroups, cl, cp); err != nil { return FailedKonnectOpError[konnectv1alpha1.KonnectGatewayControlPlane]{ Op: UpdateOp, Err: err, @@ -113,7 +131,70 @@ func updateControlPlane( } cp.Status.SetKonnectID(*resp.ControlPlane.ID) + + if err := setGroupMembers(ctx, cl, cp, sdkGroups); err != nil { + SetKonnectEntityProgrammedConditionFalse(cp, conditions.KonnectGatewayControlPlaneProgrammedReasonFailedToSetControlPlaneGroupMembers, err.Error()) + return err + } + SetKonnectEntityProgrammedCondition(cp) return nil } + +func setGroupMembers( + ctx context.Context, + cl client.Client, + cp *konnectv1alpha1.KonnectGatewayControlPlane, + sdkGroups ControlPlaneGroupSDK, +) error { + if len(cp.Spec.Members) == 0 || + cp.Spec.ClusterType == nil || + *cp.Spec.ClusterType != sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup { + return nil + } + + members, err := iter.MapErr(cp.Spec.Members, + func(member *corev1.LocalObjectReference) (sdkkonnectcomp.Members, error) { + var ( + memberCP konnectv1alpha1.KonnectGatewayControlPlane + nn = client.ObjectKey{ + Namespace: cp.Namespace, + Name: member.Name, + } + ) + if err := cl.Get(ctx, nn, &memberCP); err != nil { + return sdkkonnectcomp.Members{}, + fmt.Errorf("failed to get control plane group member %s: %w", member.Name, err) + } + if memberCP.GetKonnectID() == "" { + return sdkkonnectcomp.Members{}, + fmt.Errorf("control plane group %s member %s has no Konnect ID", cp.Name, member.Name) + } + return sdkkonnectcomp.Members{ + ID: lo.ToPtr(memberCP.GetKonnectID()), + }, nil + }) + if err != nil { + return fmt.Errorf("failed to set group members, some members couldn't be found: %w", err) + } + + sort.Sort(membersByID(members)) + gm := sdkkonnectcomp.GroupMembership{ + Members: members, + } + _, err = sdkGroups.PutControlPlanesIDGroupMemberships(ctx, cp.GetKonnectID(), &gm) + if err != nil { + return fmt.Errorf("failed to set members on control plane group %s: %w", + client.ObjectKeyFromObject(cp), err, + ) + } + + return nil +} + +type membersByID []sdkkonnectcomp.Members + +func (m membersByID) Len() int { return len(m) } +func (m membersByID) Less(i, j int) bool { return *m[i].ID < *m[j].ID } +func (m membersByID) Swap(i, j int) { m[i], m[j] = m[j], m[i] } diff --git a/controller/konnect/ops/ops_controlplane_test.go b/controller/konnect/ops/ops_controlplane_test.go index 8ff7f3066..6a3a9c1a4 100644 --- a/controller/konnect/ops/ops_controlplane_test.go +++ b/controller/konnect/ops/ops_controlplane_test.go @@ -11,11 +11,16 @@ import ( "github.com/google/uuid" "github.com/samber/lo" "github.com/stretchr/testify/assert" + mock "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8stypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/kong/gateway-operator/controller/konnect/conditions" + "github.com/kong/gateway-operator/modules/manager/scheme" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" @@ -25,14 +30,15 @@ func TestCreateControlPlane(t *testing.T) { ctx := context.Background() testCases := []struct { name string - mockCPPair func(*testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) + mockCPTuple func(*testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) expectedErr bool assertions func(*testing.T, *konnectv1alpha1.KonnectGatewayControlPlane) }{ { name: "success", - mockCPPair: func(t *testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + mockCPTuple: func(t *testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { sdk := NewMockControlPlaneSDK(t) + sdkGroups := NewMockControlPlaneGroupSDK(t) cp := &konnectv1alpha1.KonnectGatewayControlPlane{ Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ @@ -55,7 +61,7 @@ func TestCreateControlPlane(t *testing.T) { nil, ) - return sdk, cp + return sdk, sdkGroups, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { assert.Equal(t, "12345", cp.GetKonnectStatus().GetKonnectID()) @@ -68,8 +74,9 @@ func TestCreateControlPlane(t *testing.T) { }, { name: "fail", - mockCPPair: func(t *testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + mockCPTuple: func(t *testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { sdk := NewMockControlPlaneSDK(t) + sdkGroups := NewMockControlPlaneGroupSDK(t) cp := &konnectv1alpha1.KonnectGatewayControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "cp-1", @@ -95,7 +102,7 @@ func TestCreateControlPlane(t *testing.T) { }, ) - return sdk, cp + return sdk, sdkGroups, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { assert.Equal(t, "", cp.Status.GetKonnectID()) @@ -112,9 +119,10 @@ func TestCreateControlPlane(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - sdk, cp := tc.mockCPPair(t) + sdk, sdkGroups, cp := tc.mockCPTuple(t) + fakeClient := fake.NewClientBuilder().Build() - err := createControlPlane(ctx, sdk, cp) + err := createControlPlane(ctx, sdk, sdkGroups, fakeClient, cp) tc.assertions(t, cp) if tc.expectedErr { @@ -259,14 +267,15 @@ func TestUpdateControlPlane(t *testing.T) { ctx := context.Background() testCases := []struct { name string - mockCPPair func(*testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) + mockCPTuple func(*testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) expectedErr bool assertions func(*testing.T, *konnectv1alpha1.KonnectGatewayControlPlane) }{ { name: "success", - mockCPPair: func(t *testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + mockCPTuple: func(t *testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { sdk := NewMockControlPlaneSDK(t) + sdkGroups := NewMockControlPlaneGroupSDK(t) cp := &konnectv1alpha1.KonnectGatewayControlPlane{ Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ @@ -299,7 +308,7 @@ func TestUpdateControlPlane(t *testing.T) { nil, ) - return sdk, cp + return sdk, sdkGroups, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { assert.Equal(t, "12345", cp.Status.GetKonnectID()) @@ -313,8 +322,9 @@ func TestUpdateControlPlane(t *testing.T) { }, { name: "fail", - mockCPPair: func(t *testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + mockCPTuple: func(t *testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { sdk := NewMockControlPlaneSDK(t) + sdkGroups := NewMockControlPlaneGroupSDK(t) cp := &konnectv1alpha1.KonnectGatewayControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "cp-1", @@ -351,7 +361,7 @@ func TestUpdateControlPlane(t *testing.T) { }, ) - return sdk, cp + return sdk, sdkGroups, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { assert.Equal(t, "12345", cp.Status.GetKonnectID()) @@ -366,8 +376,9 @@ func TestUpdateControlPlane(t *testing.T) { }, { name: "when not found then try to create", - mockCPPair: func(t *testing.T) (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + mockCPTuple: func(t *testing.T) (*MockControlPlaneSDK, *MockControlPlaneGroupSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { sdk := NewMockControlPlaneSDK(t) + sdkGroups := NewMockControlPlaneGroupSDK(t) cp := &konnectv1alpha1.KonnectGatewayControlPlane{ ObjectMeta: metav1.ObjectMeta{ Name: "cp-1", @@ -418,7 +429,7 @@ func TestUpdateControlPlane(t *testing.T) { nil, ) - return sdk, cp + return sdk, sdkGroups, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { assert.Equal(t, "12345", cp.Status.GetKonnectID()) @@ -434,9 +445,10 @@ func TestUpdateControlPlane(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - sdk, cp := tc.mockCPPair(t) + sdk, sdkGroups, cp := tc.mockCPTuple(t) + fakeClient := fake.NewClientBuilder().Build() - err := updateControlPlane(ctx, sdk, cp) + err := updateControlPlane(ctx, sdk, sdkGroups, fakeClient, cp) if tc.assertions != nil { tc.assertions(t, cp) @@ -472,7 +484,9 @@ func TestCreateAndUpdateControlPlane_KubernetesMetadataConsistency(t *testing.T) }, }, } - sdk = NewMockControlPlaneSDK(t) + sdk = NewMockControlPlaneSDK(t) + sdkGroups = NewMockControlPlaneGroupSDK(t) + fakeClient = fake.NewClientBuilder().Build() ) t.Log("Triggering CreateControlPlane with expected labels") @@ -495,7 +509,7 @@ func TestCreateAndUpdateControlPlane_KubernetesMetadataConsistency(t *testing.T) ID: lo.ToPtr("12345"), }, }, nil) - require.NoError(t, createControlPlane(ctx, sdk, cp)) + require.NoError(t, createControlPlane(ctx, sdk, sdkGroups, fakeClient, cp)) t.Log("Triggering UpdateControlPlane with expected labels") sdk.EXPECT(). @@ -508,5 +522,278 @@ func TestCreateAndUpdateControlPlane_KubernetesMetadataConsistency(t *testing.T) ID: lo.ToPtr("12345"), }, }, nil) - require.NoError(t, updateControlPlane(ctx, sdk, cp)) + require.NoError(t, updateControlPlane(ctx, sdk, sdkGroups, fakeClient, cp)) +} + +func TestSetGroupMembers(t *testing.T) { + testcases := []struct { + name string + group *konnectv1alpha1.KonnectGatewayControlPlane + cps []client.Object + sdk func(t *testing.T) *MockControlPlaneGroupSDK + expectedErr bool + }{ + { + name: "no members", + group: &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-group", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-group", + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + }, + }, + sdk: func(t *testing.T) *MockControlPlaneGroupSDK { + sdk := NewMockControlPlaneGroupSDK(t) + return sdk + }, + }, + { + name: "1 member with Konnect Status ID", + group: &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-group", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-group", + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + Members: []corev1.LocalObjectReference{ + { + Name: "cp-1", + }, + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cpg-12345", + }, + }, + }, + cps: []client.Object{ + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12345", + }, + }, + }, + }, + sdk: func(t *testing.T) *MockControlPlaneGroupSDK { + sdk := NewMockControlPlaneGroupSDK(t) + sdk.EXPECT(). + PutControlPlanesIDGroupMemberships( + mock.Anything, + "cpg-12345", + &sdkkonnectcomp.GroupMembership{ + Members: []sdkkonnectcomp.Members{ + { + ID: lo.ToPtr("cp-12345"), + }, + }, + }, + ). + Return( + &sdkkonnectops.PutControlPlanesIDGroupMembershipsResponse{}, + nil, + ) + return sdk + }, + }, + { + name: "1 member without Konnect Status ID", + group: &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-group", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-group", + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + Members: []corev1.LocalObjectReference{ + { + Name: "cp-1", + }, + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cpg-12345", + }, + }, + }, + cps: []client.Object{ + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{}, + }, + }, + sdk: func(t *testing.T) *MockControlPlaneGroupSDK { + sdk := NewMockControlPlaneGroupSDK(t) + return sdk + }, + expectedErr: true, + }, + { + name: "2 member with Konnect Status IDs", + group: &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-group", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-group", + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + Members: []corev1.LocalObjectReference{ + { + Name: "cp-1", + }, + { + Name: "cp-2", + }, + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cpg-12345", + }, + }, + }, + cps: []client.Object{ + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12345", + }, + }, + }, + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-2", + Namespace: "default", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12346", + }, + }, + }, + }, + sdk: func(t *testing.T) *MockControlPlaneGroupSDK { + sdk := NewMockControlPlaneGroupSDK(t) + sdk.EXPECT(). + PutControlPlanesIDGroupMemberships( + mock.Anything, + "cpg-12345", + &sdkkonnectcomp.GroupMembership{ + Members: []sdkkonnectcomp.Members{ + { + ID: lo.ToPtr("cp-12345"), + }, + { + ID: lo.ToPtr("cp-12346"), + }, + }, + }, + ). + Return( + &sdkkonnectops.PutControlPlanesIDGroupMembershipsResponse{}, + nil, + ) + return sdk + }, + }, + { + name: "2 member, 1 with Konnect Status IDs, 1 without it", + group: &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-group", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-group", + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup), + }, + Members: []corev1.LocalObjectReference{ + { + Name: "cp-1", + }, + { + Name: "cp-2", + }, + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cpg-12345", + }, + }, + }, + cps: []client.Object{ + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "cp-12345", + }, + }, + }, + &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-2", + Namespace: "default", + }, + }, + }, + sdk: func(t *testing.T) *MockControlPlaneGroupSDK { + sdk := NewMockControlPlaneGroupSDK(t) + return sdk + }, + expectedErr: true, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + fakeClient := fake.NewClientBuilder(). + WithScheme(scheme.Get()). + WithObjects(tc.group). + WithObjects(tc.cps...). + Build() + + sdk := tc.sdk(t) + err := setGroupMembers(context.Background(), fakeClient, tc.group, sdk) + if tc.expectedErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } } diff --git a/controller/konnect/ops/sdkfactory.go b/controller/konnect/ops/sdkfactory.go index 7504f00a0..c904c9b7d 100644 --- a/controller/konnect/ops/sdkfactory.go +++ b/controller/konnect/ops/sdkfactory.go @@ -8,6 +8,7 @@ import ( // SDKWrapper is a wrapper of Konnect SDK to allow using mock SDKs in tests. type SDKWrapper interface { GetControlPlaneSDK() ControlPlaneSDK + GetControlPlaneGroupSDK() ControlPlaneGroupSDK GetServicesSDK() ServicesSDK GetRoutesSDK() RoutesSDK GetConsumersSDK() ConsumersSDK @@ -36,11 +37,16 @@ type sdkWrapper struct { var _ SDKWrapper = sdkWrapper{} -// GetControlPlaneSDK returns the SDK to operate Konenct control planes. +// GetControlPlaneSDK returns the SDK to operate Konnect control planes. func (w sdkWrapper) GetControlPlaneSDK() ControlPlaneSDK { return w.sdk.ControlPlanes } +// GetControlPlaneGroupSDK returns the SDK to operate Konnect control plane groups. +func (w sdkWrapper) GetControlPlaneGroupSDK() ControlPlaneGroupSDK { + return w.sdk.ControlPlaneGroups +} + // GetServicesSDK returns the SDK to operate Kong services. func (w sdkWrapper) GetServicesSDK() ServicesSDK { return w.sdk.Services diff --git a/controller/konnect/ops/sdkfactory_mock.go b/controller/konnect/ops/sdkfactory_mock.go index fd17d37b0..d06724084 100644 --- a/controller/konnect/ops/sdkfactory_mock.go +++ b/controller/konnect/ops/sdkfactory_mock.go @@ -8,6 +8,7 @@ import ( type MockSDKWrapper struct { ControlPlaneSDK *MockControlPlaneSDK + ControlPlaneGroupSDK *MockControlPlaneGroupSDK ServicesSDK *MockServicesSDK RoutesSDK *MockRoutesSDK ConsumersSDK *MockConsumersSDK @@ -35,6 +36,7 @@ var _ SDKWrapper = MockSDKWrapper{} func NewMockSDKWrapperWithT(t *testing.T) *MockSDKWrapper { return &MockSDKWrapper{ ControlPlaneSDK: NewMockControlPlaneSDK(t), + ControlPlaneGroupSDK: NewMockControlPlaneGroupSDK(t), ServicesSDK: NewMockServicesSDK(t), RoutesSDK: NewMockRoutesSDK(t), ConsumersSDK: NewMockConsumersSDK(t), @@ -62,6 +64,10 @@ func (m MockSDKWrapper) GetControlPlaneSDK() ControlPlaneSDK { return m.ControlPlaneSDK } +func (m MockSDKWrapper) GetControlPlaneGroupSDK() ControlPlaneGroupSDK { + return m.ControlPlaneGroupSDK +} + func (m MockSDKWrapper) GetServicesSDK() ServicesSDK { return m.ServicesSDK } diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index ce99c8312..4b1399099 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -497,8 +497,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } return ctrl.Result{}, ops.FailedKonnectOpError[T]{ - Op: ops.CreateOp, - + Op: ops.CreateOp, Err: err, } } @@ -526,6 +525,15 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } if res, err := ops.Update[T, TEnt](ctx, sdk, r.SyncPeriod, r.Client, ent); err != nil { + ent.GetKonnectStatus().ServerURL = apiAuth.Spec.ServerURL + ent.GetKonnectStatus().OrgID = apiAuth.Status.OrganizationID + if errUpd := r.Client.Status().Update(ctx, ent); errUpd != nil { + if k8serrors.IsConflict(errUpd) { + return ctrl.Result{Requeue: true}, nil + } + return ctrl.Result{}, fmt.Errorf("failed to update in cluster resource after Konnect update: %w %w", errUpd, err) + } + return ctrl.Result{}, fmt.Errorf("failed to update object: %w", err) } else if !res.IsZero() { return res, nil diff --git a/docs/api-reference.md b/docs/api-reference.md index 26fbf56d1..ac66ab752 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -3215,6 +3215,7 @@ KonnectGatewayControlPlaneSpec defines the desired state of KonnectGatewayContro | `cloud_gateway` _boolean_ | Whether this control-plane can be used for cloud-gateways. | | `proxy_urls` _[ProxyURL](#proxyurl) array_ | Array of proxy URLs associated with reaching the data-planes connected to a control-plane. | | `labels` _object (keys:string, values:string)_ | Labels store metadata of an entity that can be used for filtering an entity list or for searching across entity types.

Keys must be of length 1-63 characters, and cannot start with "kong", "konnect", "mesh", "kic", or "_". | +| `members` _[LocalObjectReference](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#localobjectreference-v1-core) array_ | Members is a list of references to the KonnectGatewayControlPlaneMembers that are part of this control plane group. Only applicable for ControlPlanes that are created as groups. | | `konnect` _[KonnectConfiguration](#konnectconfiguration)_ | | diff --git a/go.mod b/go.mod index f312a4495..cb0b29f67 100644 --- a/go.mod +++ b/go.mod @@ -18,12 +18,13 @@ 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.27 + github.com/kong/kubernetes-configuration v0.0.28 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 github.com/kr/pretty v0.3.1 github.com/samber/lo v1.47.0 + github.com/sourcegraph/conc v0.3.0 github.com/stretchr/testify v1.9.0 golang.org/x/mod v0.21.0 k8s.io/api v0.31.1 diff --git a/go.sum b/go.sum index e70cab284..96c7eaeff 100644 --- a/go.sum +++ b/go.sum @@ -224,8 +224,8 @@ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kong/go-kong v0.59.1 h1:AJZtyCD+Zyqe/mF/m+x3/qN/GPVxAH7jq9zGJTHRfjc= github.com/kong/go-kong v0.59.1/go.mod h1:8Vt6HmtgLNgL/7bSwAlz3DIWqBtzG7qEt9+OnMiQOa0= -github.com/kong/kubernetes-configuration v0.0.27 h1:4h1spllBnfF0P4+cr0J4xp+P5oGxAstgV8waCNkl5XI= -github.com/kong/kubernetes-configuration v0.0.27/go.mod h1:DXrWtdZzewUyPZBR4zvDoY/B8rHxeqcqCBDbyHA+B0Q= +github.com/kong/kubernetes-configuration v0.0.28 h1:3W1YcKtpRS6wrtvvvJdVzp5UT14+HpEBqAPC3rOPHTo= +github.com/kong/kubernetes-configuration v0.0.28/go.mod h1:oAdPMWiWJ6qbMPPExUSj3c3YrI675JUIfsDcKWnGW0M= 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= @@ -343,6 +343,8 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/test/envtest/konnect_entities_gatewaycontrolplane_test.go b/test/envtest/konnect_entities_gatewaycontrolplane_test.go index 32c16c026..4bd8c2224 100644 --- a/test/envtest/konnect_entities_gatewaycontrolplane_test.go +++ b/test/envtest/konnect_entities_gatewaycontrolplane_test.go @@ -17,6 +17,7 @@ import ( "github.com/kong/gateway-operator/controller/konnect/conditions" "github.com/kong/gateway-operator/controller/konnect/ops" + "github.com/kong/gateway-operator/test/helpers/deploy" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) @@ -25,65 +26,148 @@ var konnectGatewayControlPlaneTestCases = []konnectEntityReconcilerTestCase{ { name: "should create control plane successfully", objectOps: func(ctx context.Context, t *testing.T, cl client.Client, ns *corev1.Namespace) { - auth := &konnectv1alpha1.KonnectAPIAuthConfiguration{ - ObjectMeta: metav1.ObjectMeta{ - Name: "auth", - Namespace: ns.Name, + auth := deploy.KonnectAPIAuthConfigurationWithProgrammed(t, ctx, cl) + deploy.KonnectGatewayControlPlane(t, ctx, cl, auth, + func(obj client.Object) { + cp := obj.(*konnectv1alpha1.KonnectGatewayControlPlane) + cp.Name = "cp-1" + cp.Spec.Name = "cp-1" + cp.Spec.Description = lo.ToPtr("test control plane 1") }, - Spec: konnectv1alpha1.KonnectAPIAuthConfigurationSpec{ - Type: konnectv1alpha1.KonnectAPIAuthTypeToken, - Token: "kpat_test", - ServerURL: "127.0.0.1", - }, - } - require.NoError(t, cl.Create(ctx, auth)) - // We cannot create KonnectAPIAuthConfiguration with specified status, so we update the status after creating it. - auth.Status = konnectv1alpha1.KonnectAPIAuthConfigurationStatus{ - OrganizationID: "org-1", - ServerURL: "127.0.0.1", - Conditions: []metav1.Condition{ - { - Type: conditions.KonnectEntityAPIAuthConfigurationValidConditionType, - ObservedGeneration: auth.GetGeneration(), - Status: metav1.ConditionTrue, - Reason: conditions.KonnectEntityAPIAuthConfigurationReasonValid, - LastTransitionTime: metav1.Now(), + ) + }, + mockExpectations: func(t *testing.T, sdk *ops.MockSDKWrapper, ns *corev1.Namespace) { + sdk.ControlPlaneSDK.EXPECT(). + CreateControlPlane( + mock.Anything, + mock.MatchedBy(func(req sdkkonnectcomp.CreateControlPlaneRequest) bool { + return req.Name == "cp-1" && + req.Description != nil && *req.Description == "test control plane 1" + }), + ). + Return( + &sdkkonnectops.CreateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: lo.ToPtr("12345"), + }, }, - }, + nil) + // verify that mock SDK is called as expected. + t.Cleanup(func() { + require.True(t, sdk.ControlPlaneSDK.AssertExpectations(t)) + }) + }, + eventuallyPredicate: func(ctx context.Context, t *assert.CollectT, cl client.Client, ns *corev1.Namespace) { + cp := &konnectv1alpha1.KonnectGatewayControlPlane{} + if !assert.NoError(t, + cl.Get(ctx, + k8stypes.NamespacedName{ + Namespace: ns.Name, + Name: "cp-1", + }, + cp, + ), + ) { + return } - require.NoError(t, cl.Status().Update(ctx, auth)) - // Create KonnectGatewayControlPlane. - cp := &konnectv1alpha1.KonnectGatewayControlPlane{ - ObjectMeta: metav1.ObjectMeta{ - Name: "cp-1", - Namespace: ns.Name, + + assert.Equal(t, "12345", cp.Status.ID) + assert.True(t, conditionsContainProgrammedTrue(cp.Status.Conditions), + "Programmed condition should be set and it status should be true", + ) + }, + }, + { + name: "should create control plane group and control plane as member successfully", + objectOps: func(ctx context.Context, t *testing.T, cl client.Client, ns *corev1.Namespace) { + auth := deploy.KonnectAPIAuthConfigurationWithProgrammed(t, ctx, cl) + deploy.KonnectGatewayControlPlane(t, ctx, cl, auth, + func(obj client.Object) { + cp := obj.(*konnectv1alpha1.KonnectGatewayControlPlane) + cp.Name = "cp-groupmember-1" + cp.Spec.Name = "cp-groupmember-1" }, - Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ - KonnectConfiguration: konnectv1alpha1.KonnectConfiguration{ - APIAuthConfigurationRef: konnectv1alpha1.KonnectAPIAuthConfigurationRef{ - Name: "auth", + ) + deploy.KonnectGatewayControlPlane(t, ctx, cl, auth, + func(obj client.Object) { + cp := obj.(*konnectv1alpha1.KonnectGatewayControlPlane) + cp.Name = "cp-2" + cp.Spec.Name = "cp-2" + cp.Spec.ClusterType = lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlaneGroup) + cp.Spec.Members = []corev1.LocalObjectReference{ + { + Name: "cp-groupmember-1", }, - }, - CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ - Name: "cp-1", - Description: lo.ToPtr("test control plane 1"), - }, + } }, - } - require.NoError(t, cl.Create(ctx, cp)) + ) }, mockExpectations: func(t *testing.T, sdk *ops.MockSDKWrapper, ns *corev1.Namespace) { - sdk.ControlPlaneSDK.EXPECT().CreateControlPlane(mock.Anything, mock.MatchedBy(func(req sdkkonnectcomp.CreateControlPlaneRequest) bool { - return req.Name == "cp-1" && - req.Description != nil && *req.Description == "test control plane 1" - })).Return(&sdkkonnectops.CreateControlPlaneResponse{ - ControlPlane: &sdkkonnectcomp.ControlPlane{ - ID: lo.ToPtr("12345"), - }, - }, nil) + sdk.ControlPlaneSDK.EXPECT(). + CreateControlPlane( + mock.Anything, + mock.MatchedBy(func(req sdkkonnectcomp.CreateControlPlaneRequest) bool { + return req.Name == "cp-groupmember-1" + }), + ). + Return( + &sdkkonnectops.CreateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: lo.ToPtr("12345"), + }, + }, + nil) + sdk.ControlPlaneSDK.EXPECT(). + CreateControlPlane( + mock.Anything, + mock.MatchedBy(func(req sdkkonnectcomp.CreateControlPlaneRequest) bool { + return req.Name == "cp-2" + }), + ). + Return( + &sdkkonnectops.CreateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: lo.ToPtr("12346"), + }, + }, + nil) + + sdk.ControlPlaneGroupSDK.EXPECT(). + PutControlPlanesIDGroupMemberships( + mock.Anything, + "12346", + &sdkkonnectcomp.GroupMembership{ + Members: []sdkkonnectcomp.Members{ + { + ID: lo.ToPtr("12345"), + }, + }, + }, + ). + Return( + &sdkkonnectops.PutControlPlanesIDGroupMembershipsResponse{}, + nil, + ) + + sdk.ControlPlaneSDK.EXPECT(). + UpdateControlPlane( + mock.Anything, + "12346", + mock.MatchedBy(func(req sdkkonnectcomp.UpdateControlPlaneRequest) bool { + return req.Name != nil && *req.Name == "cp-2" + }), + ). + Return( + &sdkkonnectops.UpdateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: lo.ToPtr("12346"), + }, + }, + nil) // verify that mock SDK is called as expected. t.Cleanup(func() { require.True(t, sdk.ControlPlaneSDK.AssertExpectations(t)) + require.True(t, sdk.ControlPlaneGroupSDK.AssertExpectations(t)) }) }, eventuallyPredicate: func(ctx context.Context, t *assert.CollectT, cl client.Client, ns *corev1.Namespace) { @@ -92,7 +176,7 @@ var konnectGatewayControlPlaneTestCases = []konnectEntityReconcilerTestCase{ cl.Get(ctx, k8stypes.NamespacedName{ Namespace: ns.Name, - Name: "cp-1", + Name: "cp-groupmember-1", }, cp, ), @@ -101,13 +185,36 @@ var konnectGatewayControlPlaneTestCases = []konnectEntityReconcilerTestCase{ } assert.Equal(t, "12345", cp.Status.ID) - assert.True(t, - lo.ContainsBy(cp.Status.Conditions, func(condition metav1.Condition) bool { - return condition.Type == conditions.KonnectEntityProgrammedConditionType && - condition.Status == metav1.ConditionTrue - }), + assert.True(t, conditionsContainProgrammedTrue(cp.Status.Conditions), + "Programmed condition should be set and it status should be true", + ) + + cpGroup := &konnectv1alpha1.KonnectGatewayControlPlane{} + if !assert.NoError(t, + cl.Get(ctx, + k8stypes.NamespacedName{ + Namespace: ns.Name, + Name: "cp-2", + }, + cpGroup, + ), + ) { + return + } + + assert.Equal(t, "12346", cpGroup.Status.ID) + assert.True(t, conditionsContainProgrammedTrue(cpGroup.Status.Conditions), "Programmed condition should be set and it status should be true", ) }, }, } + +func conditionsContainProgrammedTrue(conds []metav1.Condition) bool { + return lo.ContainsBy(conds, + func(condition metav1.Condition) bool { + return condition.Type == conditions.KonnectEntityProgrammedConditionType && + condition.Status == metav1.ConditionTrue + }, + ) +} diff --git a/test/envtest/konnect_entities_suite_test.go b/test/envtest/konnect_entities_suite_test.go index 4240337ce..56b38869f 100644 --- a/test/envtest/konnect_entities_suite_test.go +++ b/test/envtest/konnect_entities_suite_test.go @@ -68,18 +68,18 @@ func testNewKonnectEntityReconciler[ }) require.NoError(t, err) - cl := mgr.GetClient() - factory := ops.NewMockSDKFactory(t) - sdk := factory.SDK - reconciler := konnect.NewKonnectEntityReconciler[T, TEnt](factory, false, cl) - require.NoError(t, reconciler.SetupWithManager(ctx, mgr)) - ns := &corev1.Namespace{ ObjectMeta: metav1.ObjectMeta{ Name: nsName, }, } - require.NoError(t, cl.Create(ctx, ns)) + require.NoError(t, mgr.GetClient().Create(ctx, ns)) + + cl := client.NewNamespacedClient(mgr.GetClient(), ns.Name) + factory := ops.NewMockSDKFactory(t) + sdk := factory.SDK + reconciler := konnect.NewKonnectEntityReconciler[T, TEnt](factory, false, cl) + require.NoError(t, reconciler.SetupWithManager(ctx, mgr)) t.Logf("Starting manager for test case %s", t.Name()) go func() { diff --git a/test/helpers/deploy/deploy_resources.go b/test/helpers/deploy/deploy_resources.go index b8df7bb61..9e6fa74c0 100644 --- a/test/helpers/deploy/deploy_resources.go +++ b/test/helpers/deploy/deploy_resources.go @@ -162,10 +162,11 @@ func KonnectGatewayControlPlaneWithID( ctx context.Context, cl client.Client, apiAuth *konnectv1alpha1.KonnectAPIAuthConfiguration, + opts ...objOption, ) *konnectv1alpha1.KonnectGatewayControlPlane { t.Helper() - cp := KonnectGatewayControlPlane(t, ctx, cl, apiAuth) + cp := KonnectGatewayControlPlane(t, ctx, cl, apiAuth, opts...) cp.Status.Conditions = []metav1.Condition{ { Type: conditions.KonnectEntityProgrammedConditionType,