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,