From a3b231ea17596910065f2964f00c20102afe41e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Thu, 5 Sep 2024 08:26:25 +0200 Subject: [PATCH] refactor(konnect): add SDK interface types and theirs mocks (#507) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(konnect): add SDK interface types for each entity type * tests: add mockery for Konnect SDK * tests: add mocks for KongService SDK * Resolve conflict for "refactor(konnect): add SDK interface types and theirs mocks"(#507) (#542) * feat(cli): configurable certgen images (#516) * feat(cli): configurable certgen images Add two new command line options to the manager. -webhook-certificate-config-base-image string The base image for the certgen Jobs. (default "registry.k8s.io/ingress-nginx/kube-webhook-certgen:v1.3.0") -webhook-certificate-config-shell-image string The shell image for the certgen Jobs. (default "busybox") Those are optional. If you omit them the defaults will be used which are the previous hard coded values. Signed-off-by: Arpad Kunszt * feat(cli): cleanup code per review Removed unnecessary variable assignments. Signed-off-by: Arpad Kunszt * feat(cli): add CHANGELOG entry Signed-off-by: Arpad Kunszt * feat(cli): unit tests handle new arguments Signed-off-by: Arpad Kunszt * feat(cli): move new configuration into Config Also created a constant for the shell image, so it is no more a hard coded string hidden in the code. Signed-off-by: Arpad Kunszt * feat(cli): added UT for the new arguments The new unit test tests only if the command line arguments are set in the configuration. The other cases, the default configuration, the environmental variable handling are already tested in previous cases. The test does not cover if the values from the configuration are actually used but at the moment there are no tests for that part of the code at all. Signed-off-by: Arpad Kunszt --------- Signed-off-by: Arpad Kunszt * chore(deps): bump github.com/kong/kubernetes-configuration (#523) Bumps [github.com/kong/kubernetes-configuration](https://github.com/kong/kubernetes-configuration) from 0.0.8 to 0.0.9. - [Commits](https://github.com/kong/kubernetes-configuration/compare/v0.0.8...v0.0.9) --- updated-dependencies: - dependency-name: github.com/kong/kubernetes-configuration dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/kong/kubernetes-ingress-controller/v3 from 3.2.4 to 3.3.0 (#531) * chore(deps): bump github.com/kong/kubernetes-ingress-controller/v3 Bumps [github.com/kong/kubernetes-ingress-controller/v3](https://github.com/kong/kubernetes-ingress-controller) from 3.2.4 to 3.3.0. - [Release notes](https://github.com/kong/kubernetes-ingress-controller/releases) - [Changelog](https://github.com/Kong/kubernetes-ingress-controller/blob/main/CHANGELOG.md) - [Commits](https://github.com/kong/kubernetes-ingress-controller/compare/v3.2.4...v3.3.0) --- updated-dependencies: - dependency-name: github.com/kong/kubernetes-ingress-controller/v3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * chore: go mod tidy --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jakub Warczarek * `KongPluginBinding` reconciler (#513) * feat: KongPluginBinding reconciler Signed-off-by: Mattia Lavacca * chore: fix linter Signed-off-by: Mattia Lavacca * chore: CHANGELOG updated Signed-off-by: Mattia Lavacca * Update controller/konnect/ops_kongpluginbinding.go Co-authored-by: Grzegorz Burzyński --------- Signed-off-by: Mattia Lavacca Co-authored-by: Grzegorz Burzyński * chore(deps): update kong/kubernetes-ingress-controller docker tag to v3.3.0 (#532) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * chore(deps): update kong/kubernetes-ingress-controller docker tag to v3.3.1 (#534) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * Add watch for KongPlugin and KongClusterPlugin in KongPluginBinding controller (#535) * chore(deps): bump github.com/kong/kubernetes-ingress-controller/v3 (#536) Bumps [github.com/kong/kubernetes-ingress-controller/v3](https://github.com/kong/kubernetes-ingress-controller) from 3.3.0 to 3.3.1. - [Release notes](https://github.com/kong/kubernetes-ingress-controller/releases) - [Changelog](https://github.com/Kong/kubernetes-ingress-controller/blob/main/CHANGELOG.md) - [Commits](https://github.com/kong/kubernetes-ingress-controller/compare/v3.3.0...v3.3.1) --- updated-dependencies: - dependency-name: github.com/kong/kubernetes-ingress-controller/v3 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump github.com/Masterminds/sprig/v3 in /hack/generators (#539) Bumps [github.com/Masterminds/sprig/v3](https://github.com/Masterminds/sprig) from 3.2.3 to 3.3.0. - [Release notes](https://github.com/Masterminds/sprig/releases) - [Changelog](https://github.com/Masterminds/sprig/blob/master/CHANGELOG.md) - [Commits](https://github.com/Masterminds/sprig/compare/v3.2.3...v3.3.0) --- updated-dependencies: - dependency-name: github.com/Masterminds/sprig/v3 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): update dependency kubernetes-sigs/controller-tools to v0.16.2 (#538) * chore(deps): update dependency kubernetes-sigs/controller-tools to v0.16.2 * chore: regenerate --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jakub Warczarek * chore(deps): bump github.com/gruntwork-io/terratest (#541) Bumps [github.com/gruntwork-io/terratest](https://github.com/gruntwork-io/terratest) from 0.47.0 to 0.47.1. - [Release notes](https://github.com/gruntwork-io/terratest/releases) - [Commits](https://github.com/gruntwork-io/terratest/compare/v0.47.0...v0.47.1) --- updated-dependencies: - dependency-name: github.com/gruntwork-io/terratest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * update golangci configuration --------- Signed-off-by: Arpad Kunszt Signed-off-by: dependabot[bot] Signed-off-by: Mattia Lavacca Co-authored-by: akunszt <32456696+akunszt@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jakub Warczarek Co-authored-by: Mattia Lavacca Co-authored-by: Grzegorz Burzyński Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> * fix lint * replace KonnnectControlPlane to KonnectGatewayControlPlane --------- Signed-off-by: Arpad Kunszt Signed-off-by: dependabot[bot] Signed-off-by: Mattia Lavacca Co-authored-by: Tao Yi Co-authored-by: akunszt <32456696+akunszt@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Jakub Warczarek Co-authored-by: Mattia Lavacca Co-authored-by: Grzegorz Burzyński Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .gitattributes | 1 + .golangci.yaml | 7 + .mockery.yaml | 15 + .tools_versions.yaml | 2 + Makefile | 14 +- .../konnect/{ => conditions}/conditions.go | 2 +- .../konnect/{ => constraints}/constraints.go | 6 +- .../konnect/constraints/entitytypename.go | 7 + controller/konnect/entitytypename.go | 6 - controller/konnect/errors.go | 19 - controller/konnect/index.go | 6 +- controller/konnect/ops/controlplane.go | 15 + .../konnect/ops/controlplane_mock_test.go | 263 +++++++++ controller/konnect/ops/errors.go | 26 + controller/konnect/ops/kongconsumer.go | 15 + controller/konnect/ops/kongconsumergroup.go | 15 + controller/konnect/ops/kongroute.go | 15 + controller/konnect/ops/kongservice.go | 15 + .../konnect/ops/kongservice_mock_test.go | 264 +++++++++ controller/konnect/{ => ops}/ops.go | 69 +-- .../konnect/{ => ops}/ops_controlplane.go | 41 +- .../konnect/ops/ops_controlplane_test.go | 450 ++++++++++++++++ .../konnect/{ => ops}/ops_kongconsumer.go | 42 +- .../{ => ops}/ops_kongconsumergroup.go | 42 +- .../{ => ops}/ops_kongpluginbinding.go | 33 +- controller/konnect/{ => ops}/ops_kongroute.go | 43 +- .../konnect/{ => ops}/ops_kongservice.go | 105 ++-- .../konnect/ops/ops_kongservice_test.go | 501 ++++++++++++++++++ controller/konnect/reconciler_generic.go | 126 ++--- controller/konnect/reconciler_generic_test.go | 5 +- .../konnect/reconciler_konnectapiauth.go | 25 +- controller/konnect/sdkfactory.go | 4 +- controller/konnect/watch.go | 6 +- controller/konnect/watch_test.go | 6 +- go.mod | 1 + test/integration/test_konnect_entities.go | 6 +- 36 files changed, 1921 insertions(+), 297 deletions(-) create mode 100644 .mockery.yaml rename controller/konnect/{ => conditions}/conditions.go (99%) rename controller/konnect/{ => constraints}/constraints.go (91%) create mode 100644 controller/konnect/constraints/entitytypename.go delete mode 100644 controller/konnect/entitytypename.go create mode 100644 controller/konnect/ops/controlplane.go create mode 100644 controller/konnect/ops/controlplane_mock_test.go create mode 100644 controller/konnect/ops/errors.go create mode 100644 controller/konnect/ops/kongconsumer.go create mode 100644 controller/konnect/ops/kongconsumergroup.go create mode 100644 controller/konnect/ops/kongroute.go create mode 100644 controller/konnect/ops/kongservice.go create mode 100644 controller/konnect/ops/kongservice_mock_test.go rename controller/konnect/{ => ops}/ops.go (74%) rename controller/konnect/{ => ops}/ops_controlplane.go (76%) create mode 100644 controller/konnect/ops/ops_controlplane_test.go rename controller/konnect/{ => ops}/ops_kongconsumer.go (82%) rename controller/konnect/{ => ops}/ops_kongconsumergroup.go (82%) rename controller/konnect/{ => ops}/ops_kongpluginbinding.go (90%) rename controller/konnect/{ => ops}/ops_kongroute.go (84%) rename controller/konnect/{ => ops}/ops_kongservice.go (67%) create mode 100644 controller/konnect/ops/ops_kongservice_test.go diff --git a/.gitattributes b/.gitattributes index 8f1c1fee6..5a81cdb4f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ **/zz_generated*.go linguist-generated=true pkg/clientset/** linguist-generated=true +controller/konnect/ops/*_mock_test.go linguist-generated=true diff --git a/.golangci.yaml b/.golangci.yaml index c99fe7bc8..d4074e251 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -62,6 +62,13 @@ linters-settings: - pkg: github.com/kong/gateway-operator/internal/types alias: gwtypes + + - pkg: github.com/Kong/sdk-konnect-go/models/components + alias: sdkkonnectcomp + - pkg: github.com/Kong/sdk-konnect-go/models/operations + alias: sdkkonnectops + - pkg: github.com/Kong/sdk-konnect-go/models/sdkerrors + alias: sdkkonnecterrs revive: rules: - name: errorf diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 000000000..36d799f63 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,15 @@ +quiet: False +inpackage: True +disable-version-string: True +with-expecter: True + +filename: "{{ trimSuffix .InterfaceFile \".go\" | base | lower }}_mock_test.go" +dir: "{{ .InterfaceDir }}" +mockname: "Mock{{ .InterfaceName }}" +outpkg: "{{ .PackageName }}" + +packages: + github.com/kong/gateway-operator/controller/konnect/ops: + interfaces: + ControlPlaneSDK: + ServicesSDK: diff --git a/.tools_versions.yaml b/.tools_versions.yaml index d7daa6871..c94f35896 100644 --- a/.tools_versions.yaml +++ b/.tools_versions.yaml @@ -14,3 +14,5 @@ dlv: "1.23.0" gotestsum: "1.12.0" # renovate: datasource=github-releases depName=elastic/crd-ref-docs crd-ref-docs: "0.1.0" +# renovate: datasource=github-releases depName=vektra/mockery +mockery: "2.44.2" diff --git a/Makefile b/Makefile index cd972bbe3..62dc511fc 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,13 @@ skaffold: mise yq ## Download skaffold locally if necessary. @$(MISE) plugin install --yes -q skaffold @$(MISE) install -q skaffold@$(SKAFFOLD_VERSION) +MOCKERY_VERSION = $(shell $(YQ) -r '.mockery' < $(TOOLS_VERSIONS_FILE)) +MOCKERY = $(PROJECT_DIR)/bin/installs/mockery/$(MOCKERY_VERSION)/bin/mockery +.PHONY: mockery +mockery: mise yq ## Download mockery locally if necessary. + @$(MISE) plugin install --yes -q mockery https://github.com/cabify/asdf-mockery.git + @$(MISE) install -q mockery@$(MOCKERY_VERSION) + # ------------------------------------------------------------------------------ # Build # ------------------------------------------------------------------------------ @@ -191,7 +198,7 @@ verify.generators: verify.repo generate verify.diff API_DIR ?= api .PHONY: generate -generate: generate.api generate.clientsets generate.rbacs generate.gateway-api-urls generate.docs generate.k8sio-gomod-replace generate.testcases-registration generate.kic-webhook-config +generate: generate.api generate.clientsets generate.rbacs generate.gateway-api-urls generate.docs generate.k8sio-gomod-replace generate.testcases-registration generate.kic-webhook-config generate.mocks .PHONY: generate.api generate.api: controller-gen @@ -388,6 +395,11 @@ test.conformance: test.samples: kustomize find ./config/samples -not -name "kustomization.*" -type f | sort | xargs -I{} bash -c "kubectl apply -f {}; kubectl delete -f {}" +# https://github.com/vektra/mockery/issues/803#issuecomment-2287198024 +.PHONY: generate.mocks +generate.mocks: mockery + GODEBUG=gotypesalias=0 $(MOCKERY) + # ------------------------------------------------------------------------------ # Gateway API # ------------------------------------------------------------------------------ diff --git a/controller/konnect/conditions.go b/controller/konnect/conditions/conditions.go similarity index 99% rename from controller/konnect/conditions.go rename to controller/konnect/conditions/conditions.go index 0a0e1e226..bce9d594c 100644 --- a/controller/konnect/conditions.go +++ b/controller/konnect/conditions/conditions.go @@ -1,4 +1,4 @@ -package konnect +package conditions // TODO(pmalek): move this to Konnect API directory so that it's part of the API contract. // https://github.com/Kong/kubernetes-configuration/issues/14 diff --git a/controller/konnect/constraints.go b/controller/konnect/constraints/constraints.go similarity index 91% rename from controller/konnect/constraints.go rename to controller/konnect/constraints/constraints.go index 52bba73e5..d3775a688 100644 --- a/controller/konnect/constraints.go +++ b/controller/konnect/constraints/constraints.go @@ -1,4 +1,4 @@ -package konnect +package constraints import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -25,9 +25,9 @@ type SupportedKonnectEntityType interface { } // EntityType is an interface that all Konnect entity types must implement. -// Separating this from SupportedKonnectEntityType allows us to use EntityType +// Separating this from constraints.SupportedKonnectEntityType allows us to use EntityType // where client.Object is required, since it embeds client.Object and uses pointer -// to refer to the SupportedKonnectEntityType. +// to refer to the constraints.SupportedKonnectEntityType. type EntityType[T SupportedKonnectEntityType] interface { *T // Kubernetes Object methods diff --git a/controller/konnect/constraints/entitytypename.go b/controller/konnect/constraints/entitytypename.go new file mode 100644 index 000000000..4dfc22b33 --- /dev/null +++ b/controller/konnect/constraints/entitytypename.go @@ -0,0 +1,7 @@ +package constraints + +// EntityTypeName returns the name of the entity type. +func EntityTypeName[T SupportedKonnectEntityType]() string { + var e T + return e.GetTypeName() +} diff --git a/controller/konnect/entitytypename.go b/controller/konnect/entitytypename.go deleted file mode 100644 index a75f73ff7..000000000 --- a/controller/konnect/entitytypename.go +++ /dev/null @@ -1,6 +0,0 @@ -package konnect - -func entityTypeName[T SupportedKonnectEntityType]() string { - var e T - return e.GetTypeName() -} diff --git a/controller/konnect/errors.go b/controller/konnect/errors.go index d95e93523..07c11aa2c 100644 --- a/controller/konnect/errors.go +++ b/controller/konnect/errors.go @@ -6,25 +6,6 @@ import ( "k8s.io/apimachinery/pkg/types" ) -// FailedKonnectOpError is an error type that is returned when an operation against -// Konnect API fails. -type FailedKonnectOpError[T SupportedKonnectEntityType] struct { - Op Op - Err error -} - -// Error implements the error interface. -func (e FailedKonnectOpError[T]) Error() string { - return fmt.Sprintf("failed to %s %s on Konnect: %v", - e.Op, entityTypeName[T](), e.Err, - ) -} - -// Unwrap returns the underlying error. -func (e FailedKonnectOpError[T]) Unwrap() error { - return e.Err -} - // ReferencedControlPlaneDoesNotExistError is an error type that is returned when // a Konnect entity references a KonnectGatewayControlPlane that does not exist. type ReferencedControlPlaneDoesNotExistError struct { diff --git a/controller/konnect/index.go b/controller/konnect/index.go index 5314b61f1..b021a29cf 100644 --- a/controller/konnect/index.go +++ b/controller/konnect/index.go @@ -3,6 +3,8 @@ package konnect import ( "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/kong/gateway-operator/controller/konnect/constraints" + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" ) @@ -14,8 +16,8 @@ type ReconciliationIndexOption struct { } // ReconciliationIndexOptionsForEntity returns required index options for controller reconciliing the entity. -func ReconciliationIndexOptionsForEntity[T SupportedKonnectEntityType, - TEnt EntityType[T]](ent TEnt) []ReconciliationIndexOption { +func ReconciliationIndexOptionsForEntity[T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T]](ent TEnt) []ReconciliationIndexOption { switch any(ent).(type) { //nolint:gocritic // TODO: add index options required for other entities case *configurationv1alpha1.KongPluginBinding: return IndexOptionsForKongPluginBinding() diff --git a/controller/konnect/ops/controlplane.go b/controller/konnect/ops/controlplane.go new file mode 100644 index 000000000..4da05c1bd --- /dev/null +++ b/controller/konnect/ops/controlplane.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// ControlPlaneSDK is the interface for the Konnect ControlPlaneSDK 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) + UpdateControlPlane(ctx context.Context, id string, req sdkkonnectcomp.UpdateControlPlaneRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpdateControlPlaneResponse, error) +} diff --git a/controller/konnect/ops/controlplane_mock_test.go b/controller/konnect/ops/controlplane_mock_test.go new file mode 100644 index 000000000..006ab5a77 --- /dev/null +++ b/controller/konnect/ops/controlplane_mock_test.go @@ -0,0 +1,263 @@ +// 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" +) + +// MockControlPlaneSDK is an autogenerated mock type for the ControlPlaneSDK type +type MockControlPlaneSDK struct { + mock.Mock +} + +type MockControlPlaneSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockControlPlaneSDK) EXPECT() *MockControlPlaneSDK_Expecter { + return &MockControlPlaneSDK_Expecter{mock: &_m.Mock} +} + +// CreateControlPlane provides a mock function with given fields: ctx, req, opts +func (_m *MockControlPlaneSDK) CreateControlPlane(ctx context.Context, req components.CreateControlPlaneRequest, opts ...operations.Option) (*operations.CreateControlPlaneResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateControlPlane") + } + + var r0 *operations.CreateControlPlaneResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, components.CreateControlPlaneRequest, ...operations.Option) (*operations.CreateControlPlaneResponse, error)); ok { + return rf(ctx, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, components.CreateControlPlaneRequest, ...operations.Option) *operations.CreateControlPlaneResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateControlPlaneResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, components.CreateControlPlaneRequest, ...operations.Option) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockControlPlaneSDK_CreateControlPlane_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateControlPlane' +type MockControlPlaneSDK_CreateControlPlane_Call struct { + *mock.Call +} + +// CreateControlPlane is a helper method to define mock.On call +// - ctx context.Context +// - req components.CreateControlPlaneRequest +// - opts ...operations.Option +func (_e *MockControlPlaneSDK_Expecter) CreateControlPlane(ctx interface{}, req interface{}, opts ...interface{}) *MockControlPlaneSDK_CreateControlPlane_Call { + return &MockControlPlaneSDK_CreateControlPlane_Call{Call: _e.mock.On("CreateControlPlane", + append([]interface{}{ctx, req}, opts...)...)} +} + +func (_c *MockControlPlaneSDK_CreateControlPlane_Call) Run(run func(ctx context.Context, req components.CreateControlPlaneRequest, opts ...operations.Option)) *MockControlPlaneSDK_CreateControlPlane_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(components.CreateControlPlaneRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockControlPlaneSDK_CreateControlPlane_Call) Return(_a0 *operations.CreateControlPlaneResponse, _a1 error) *MockControlPlaneSDK_CreateControlPlane_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockControlPlaneSDK_CreateControlPlane_Call) RunAndReturn(run func(context.Context, components.CreateControlPlaneRequest, ...operations.Option) (*operations.CreateControlPlaneResponse, error)) *MockControlPlaneSDK_CreateControlPlane_Call { + _c.Call.Return(run) + return _c +} + +// DeleteControlPlane provides a mock function with given fields: ctx, id, opts +func (_m *MockControlPlaneSDK) DeleteControlPlane(ctx context.Context, id string, opts ...operations.Option) (*operations.DeleteControlPlaneResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteControlPlane") + } + + var r0 *operations.DeleteControlPlaneResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, ...operations.Option) (*operations.DeleteControlPlaneResponse, error)); ok { + return rf(ctx, id, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, ...operations.Option) *operations.DeleteControlPlaneResponse); ok { + r0 = rf(ctx, id, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteControlPlaneResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, ...operations.Option) error); ok { + r1 = rf(ctx, id, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockControlPlaneSDK_DeleteControlPlane_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteControlPlane' +type MockControlPlaneSDK_DeleteControlPlane_Call struct { + *mock.Call +} + +// DeleteControlPlane is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - opts ...operations.Option +func (_e *MockControlPlaneSDK_Expecter) DeleteControlPlane(ctx interface{}, id interface{}, opts ...interface{}) *MockControlPlaneSDK_DeleteControlPlane_Call { + return &MockControlPlaneSDK_DeleteControlPlane_Call{Call: _e.mock.On("DeleteControlPlane", + append([]interface{}{ctx, id}, opts...)...)} +} + +func (_c *MockControlPlaneSDK_DeleteControlPlane_Call) Run(run func(ctx context.Context, id string, opts ...operations.Option)) *MockControlPlaneSDK_DeleteControlPlane_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockControlPlaneSDK_DeleteControlPlane_Call) Return(_a0 *operations.DeleteControlPlaneResponse, _a1 error) *MockControlPlaneSDK_DeleteControlPlane_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockControlPlaneSDK_DeleteControlPlane_Call) RunAndReturn(run func(context.Context, string, ...operations.Option) (*operations.DeleteControlPlaneResponse, error)) *MockControlPlaneSDK_DeleteControlPlane_Call { + _c.Call.Return(run) + return _c +} + +// UpdateControlPlane provides a mock function with given fields: ctx, id, req, opts +func (_m *MockControlPlaneSDK) UpdateControlPlane(ctx context.Context, id string, req components.UpdateControlPlaneRequest, opts ...operations.Option) (*operations.UpdateControlPlaneResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, id, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpdateControlPlane") + } + + var r0 *operations.UpdateControlPlaneResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, components.UpdateControlPlaneRequest, ...operations.Option) (*operations.UpdateControlPlaneResponse, error)); ok { + return rf(ctx, id, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, components.UpdateControlPlaneRequest, ...operations.Option) *operations.UpdateControlPlaneResponse); ok { + r0 = rf(ctx, id, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpdateControlPlaneResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, components.UpdateControlPlaneRequest, ...operations.Option) error); ok { + r1 = rf(ctx, id, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockControlPlaneSDK_UpdateControlPlane_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateControlPlane' +type MockControlPlaneSDK_UpdateControlPlane_Call struct { + *mock.Call +} + +// UpdateControlPlane is a helper method to define mock.On call +// - ctx context.Context +// - id string +// - req components.UpdateControlPlaneRequest +// - opts ...operations.Option +func (_e *MockControlPlaneSDK_Expecter) UpdateControlPlane(ctx interface{}, id interface{}, req interface{}, opts ...interface{}) *MockControlPlaneSDK_UpdateControlPlane_Call { + return &MockControlPlaneSDK_UpdateControlPlane_Call{Call: _e.mock.On("UpdateControlPlane", + append([]interface{}{ctx, id, req}, opts...)...)} +} + +func (_c *MockControlPlaneSDK_UpdateControlPlane_Call) Run(run func(ctx context.Context, id string, req components.UpdateControlPlaneRequest, opts ...operations.Option)) *MockControlPlaneSDK_UpdateControlPlane_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.UpdateControlPlaneRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockControlPlaneSDK_UpdateControlPlane_Call) Return(_a0 *operations.UpdateControlPlaneResponse, _a1 error) *MockControlPlaneSDK_UpdateControlPlane_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockControlPlaneSDK_UpdateControlPlane_Call) RunAndReturn(run func(context.Context, string, components.UpdateControlPlaneRequest, ...operations.Option) (*operations.UpdateControlPlaneResponse, error)) *MockControlPlaneSDK_UpdateControlPlane_Call { + _c.Call.Return(run) + return _c +} + +// NewMockControlPlaneSDK creates a new instance of MockControlPlaneSDK. 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 NewMockControlPlaneSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockControlPlaneSDK { + mock := &MockControlPlaneSDK{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/controller/konnect/ops/errors.go b/controller/konnect/ops/errors.go new file mode 100644 index 000000000..e4034c64e --- /dev/null +++ b/controller/konnect/ops/errors.go @@ -0,0 +1,26 @@ +package ops + +import ( + "fmt" + + "github.com/kong/gateway-operator/controller/konnect/constraints" +) + +// FailedKonnectOpError is an error type that is returned when an operation against +// Konnect API fails. +type FailedKonnectOpError[T constraints.SupportedKonnectEntityType] struct { + Op Op + Err error +} + +// Error implements the error interface. +func (e FailedKonnectOpError[T]) Error() string { + return fmt.Sprintf("failed to %s %s on Konnect: %v", + e.Op, constraints.EntityTypeName[T](), e.Err, + ) +} + +// Unwrap returns the underlying error. +func (e FailedKonnectOpError[T]) Unwrap() error { + return e.Err +} diff --git a/controller/konnect/ops/kongconsumer.go b/controller/konnect/ops/kongconsumer.go new file mode 100644 index 000000000..547ac6480 --- /dev/null +++ b/controller/konnect/ops/kongconsumer.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// ConsumersSDK is the interface for the Konnect Consumers SDK. +type ConsumersSDK interface { + CreateConsumer(ctx context.Context, controlPlaneID string, consumerInput sdkkonnectcomp.ConsumerInput, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateConsumerResponse, error) + UpsertConsumer(ctx context.Context, upsertConsumerRequest sdkkonnectops.UpsertConsumerRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertConsumerResponse, error) + DeleteConsumer(ctx context.Context, controlPlaneID string, consumerID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteConsumerResponse, error) +} diff --git a/controller/konnect/ops/kongconsumergroup.go b/controller/konnect/ops/kongconsumergroup.go new file mode 100644 index 000000000..afebcd8fd --- /dev/null +++ b/controller/konnect/ops/kongconsumergroup.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// ConsumerGroupSDK is the interface for the Konnect ConsumerGroups SDK. +type ConsumerGroupSDK interface { + CreateConsumerGroup(ctx context.Context, controlPlaneID string, consumerInput sdkkonnectcomp.ConsumerGroupInput, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateConsumerGroupResponse, error) + UpsertConsumerGroup(ctx context.Context, upsertConsumerRequest sdkkonnectops.UpsertConsumerGroupRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertConsumerGroupResponse, error) + DeleteConsumerGroup(ctx context.Context, controlPlaneID string, consumerID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteConsumerGroupResponse, error) +} diff --git a/controller/konnect/ops/kongroute.go b/controller/konnect/ops/kongroute.go new file mode 100644 index 000000000..809382866 --- /dev/null +++ b/controller/konnect/ops/kongroute.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// RoutesSDK is the interface for the Konnect Routes SDK. +type RoutesSDK interface { + CreateRoute(ctx context.Context, controlPlaneID string, route sdkkonnectcomp.RouteInput, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateRouteResponse, error) + UpsertRoute(ctx context.Context, req sdkkonnectops.UpsertRouteRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertRouteResponse, error) + DeleteRoute(ctx context.Context, controlPlaneID, routeID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteRouteResponse, error) +} diff --git a/controller/konnect/ops/kongservice.go b/controller/konnect/ops/kongservice.go new file mode 100644 index 000000000..502ae5649 --- /dev/null +++ b/controller/konnect/ops/kongservice.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" +) + +// ServicesSDK is the interface for the Konnect Service SDK. +type ServicesSDK interface { + CreateService(ctx context.Context, controlPlaneID string, service sdkkonnectcomp.ServiceInput, opts ...sdkkonnectops.Option) (*sdkkonnectops.CreateServiceResponse, error) + UpsertService(ctx context.Context, req sdkkonnectops.UpsertServiceRequest, opts ...sdkkonnectops.Option) (*sdkkonnectops.UpsertServiceResponse, error) + DeleteService(ctx context.Context, controlPlaneID, serviceID string, opts ...sdkkonnectops.Option) (*sdkkonnectops.DeleteServiceResponse, error) +} diff --git a/controller/konnect/ops/kongservice_mock_test.go b/controller/konnect/ops/kongservice_mock_test.go new file mode 100644 index 000000000..9340df0f0 --- /dev/null +++ b/controller/konnect/ops/kongservice_mock_test.go @@ -0,0 +1,264 @@ +// Code generated by mockery. DO NOT EDIT. + +package ops + +import ( + context "context" + + components "github.com/Kong/sdk-konnect-go/models/components" + + mock "github.com/stretchr/testify/mock" + + operations "github.com/Kong/sdk-konnect-go/models/operations" +) + +// MockServicesSDK is an autogenerated mock type for the ServicesSDK type +type MockServicesSDK struct { + mock.Mock +} + +type MockServicesSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockServicesSDK) EXPECT() *MockServicesSDK_Expecter { + return &MockServicesSDK_Expecter{mock: &_m.Mock} +} + +// CreateService provides a mock function with given fields: ctx, controlPlaneID, service, opts +func (_m *MockServicesSDK) CreateService(ctx context.Context, controlPlaneID string, service components.ServiceInput, opts ...operations.Option) (*operations.CreateServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, service) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 *operations.CreateServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, components.ServiceInput, ...operations.Option) (*operations.CreateServiceResponse, error)); ok { + return rf(ctx, controlPlaneID, service, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, components.ServiceInput, ...operations.Option) *operations.CreateServiceResponse); ok { + r0 = rf(ctx, controlPlaneID, service, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, components.ServiceInput, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, service, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_CreateService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateService' +type MockServicesSDK_CreateService_Call struct { + *mock.Call +} + +// CreateService is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - service components.ServiceInput +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) CreateService(ctx interface{}, controlPlaneID interface{}, service interface{}, opts ...interface{}) *MockServicesSDK_CreateService_Call { + return &MockServicesSDK_CreateService_Call{Call: _e.mock.On("CreateService", + append([]interface{}{ctx, controlPlaneID, service}, opts...)...)} +} + +func (_c *MockServicesSDK_CreateService_Call) Run(run func(ctx context.Context, controlPlaneID string, service components.ServiceInput, opts ...operations.Option)) *MockServicesSDK_CreateService_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.ServiceInput), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_CreateService_Call) Return(_a0 *operations.CreateServiceResponse, _a1 error) *MockServicesSDK_CreateService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_CreateService_Call) RunAndReturn(run func(context.Context, string, components.ServiceInput, ...operations.Option) (*operations.CreateServiceResponse, error)) *MockServicesSDK_CreateService_Call { + _c.Call.Return(run) + return _c +} + +// DeleteService provides a mock function with given fields: ctx, controlPlaneID, serviceID, opts +func (_m *MockServicesSDK) DeleteService(ctx context.Context, controlPlaneID string, serviceID string, opts ...operations.Option) (*operations.DeleteServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, serviceID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 *operations.DeleteServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) (*operations.DeleteServiceResponse, error)); ok { + return rf(ctx, controlPlaneID, serviceID, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) *operations.DeleteServiceResponse); ok { + r0 = rf(ctx, controlPlaneID, serviceID, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, serviceID, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_DeleteService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteService' +type MockServicesSDK_DeleteService_Call struct { + *mock.Call +} + +// DeleteService is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - serviceID string +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) DeleteService(ctx interface{}, controlPlaneID interface{}, serviceID interface{}, opts ...interface{}) *MockServicesSDK_DeleteService_Call { + return &MockServicesSDK_DeleteService_Call{Call: _e.mock.On("DeleteService", + append([]interface{}{ctx, controlPlaneID, serviceID}, opts...)...)} +} + +func (_c *MockServicesSDK_DeleteService_Call) Run(run func(ctx context.Context, controlPlaneID string, serviceID string, opts ...operations.Option)) *MockServicesSDK_DeleteService_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_DeleteService_Call) Return(_a0 *operations.DeleteServiceResponse, _a1 error) *MockServicesSDK_DeleteService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_DeleteService_Call) RunAndReturn(run func(context.Context, string, string, ...operations.Option) (*operations.DeleteServiceResponse, error)) *MockServicesSDK_DeleteService_Call { + _c.Call.Return(run) + return _c +} + +// UpsertService provides a mock function with given fields: ctx, req, opts +func (_m *MockServicesSDK) UpsertService(ctx context.Context, req operations.UpsertServiceRequest, opts ...operations.Option) (*operations.UpsertServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpsertService") + } + + var r0 *operations.UpsertServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) (*operations.UpsertServiceResponse, error)); ok { + return rf(ctx, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) *operations.UpsertServiceResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpsertServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_UpsertService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertService' +type MockServicesSDK_UpsertService_Call struct { + *mock.Call +} + +// UpsertService is a helper method to define mock.On call +// - ctx context.Context +// - req operations.UpsertServiceRequest +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) UpsertService(ctx interface{}, req interface{}, opts ...interface{}) *MockServicesSDK_UpsertService_Call { + return &MockServicesSDK_UpsertService_Call{Call: _e.mock.On("UpsertService", + append([]interface{}{ctx, req}, opts...)...)} +} + +func (_c *MockServicesSDK_UpsertService_Call) Run(run func(ctx context.Context, req operations.UpsertServiceRequest, opts ...operations.Option)) *MockServicesSDK_UpsertService_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(operations.UpsertServiceRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_UpsertService_Call) Return(_a0 *operations.UpsertServiceResponse, _a1 error) *MockServicesSDK_UpsertService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_UpsertService_Call) RunAndReturn(run func(context.Context, operations.UpsertServiceRequest, ...operations.Option) (*operations.UpsertServiceResponse, error)) *MockServicesSDK_UpsertService_Call { + _c.Call.Return(run) + return _c +} + +// NewMockServicesSDK creates a new instance of MockServicesSDK. 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 NewMockServicesSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockServicesSDK { + mock := &MockServicesSDK{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/controller/konnect/ops.go b/controller/konnect/ops/ops.go similarity index 74% rename from controller/konnect/ops.go rename to controller/konnect/ops/ops.go index eba53f885..a5158a574 100644 --- a/controller/konnect/ops.go +++ b/controller/konnect/ops/ops.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -11,6 +11,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" + "github.com/kong/gateway-operator/controller/konnect/constraints" "github.com/kong/gateway-operator/controller/pkg/log" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" @@ -40,8 +42,8 @@ const ( // Create creates a Konnect entity. func Create[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ]( ctx context.Context, sdk *sdkkonnectgo.SDK, @@ -52,15 +54,15 @@ func Create[ switch ent := any(e).(type) { case *konnectv1alpha1.KonnectGatewayControlPlane: - return e, createControlPlane(ctx, sdk, ent) + return e, createControlPlane(ctx, sdk.ControlPlanes, ent) case *configurationv1alpha1.KongService: - return e, createService(ctx, sdk, ent) + return e, createService(ctx, sdk.Services, ent) case *configurationv1alpha1.KongRoute: - return e, createRoute(ctx, sdk, ent) + return e, createRoute(ctx, sdk.Routes, ent) case *configurationv1.KongConsumer: - return e, createConsumer(ctx, sdk, ent) + return e, createConsumer(ctx, sdk.Consumers, ent) case *configurationv1beta1.KongConsumerGroup: - return e, createConsumerGroup(ctx, sdk, ent) + return e, createConsumerGroup(ctx, sdk.ConsumerGroups, ent) case *configurationv1alpha1.KongPluginBinding: return e, createPlugin(ctx, cl, sdk, ent) @@ -75,8 +77,8 @@ func Create[ // Delete deletes a Konnect entity. // It returns an error if the entity does not have a Konnect ID or if the operation fails. func Delete[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ](ctx context.Context, sdk *sdkkonnectgo.SDK, cl client.Client, e *T) error { ent := TEnt(e) if ent.GetKonnectStatus().GetKonnectID() == "" { @@ -90,15 +92,15 @@ func Delete[ switch ent := any(e).(type) { case *konnectv1alpha1.KonnectGatewayControlPlane: - return deleteControlPlane(ctx, sdk, ent) + return deleteControlPlane(ctx, sdk.ControlPlanes, ent) case *configurationv1alpha1.KongService: - return deleteService(ctx, sdk, ent) + return deleteService(ctx, sdk.Services, ent) case *configurationv1alpha1.KongRoute: - return deleteRoute(ctx, sdk, ent) + return deleteRoute(ctx, sdk.Routes, ent) case *configurationv1.KongConsumer: - return deleteConsumer(ctx, sdk, ent) + return deleteConsumer(ctx, sdk.Consumers, ent) case *configurationv1beta1.KongConsumerGroup: - return deleteConsumerGroup(ctx, sdk, ent) + return deleteConsumerGroup(ctx, sdk.ConsumerGroups, ent) case *configurationv1alpha1.KongPluginBinding: return deletePlugin(ctx, sdk, ent) @@ -113,12 +115,12 @@ func Delete[ // Update updates a Konnect entity. // It returns an error if the entity does not have a Konnect ID or if the operation fails. func Update[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ](ctx context.Context, sdk *sdkkonnectgo.SDK, syncPeriod time.Duration, cl client.Client, e *T) (ctrl.Result, error) { var ( ent = TEnt(e) - condProgrammed, ok = k8sutils.GetCondition(KonnectEntityProgrammedConditionType, ent) + condProgrammed, ok = k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, ent) now = time.Now() timeFromLastUpdate = time.Since(condProgrammed.LastTransitionTime.Time) ) @@ -126,7 +128,7 @@ func Update[ // the configured sync period, requeue after the remaining time. if ok && condProgrammed.Status == metav1.ConditionTrue && - condProgrammed.Reason == KonnectEntityProgrammedReasonProgrammed && + condProgrammed.Reason == conditions.KonnectEntityProgrammedReasonProgrammed && condProgrammed.ObservedGeneration == ent.GetObjectMeta().GetGeneration() && timeFromLastUpdate <= syncPeriod { requeueAfter := syncPeriod - timeFromLastUpdate @@ -153,15 +155,15 @@ func Update[ switch ent := any(e).(type) { case *konnectv1alpha1.KonnectGatewayControlPlane: - return ctrl.Result{}, updateControlPlane(ctx, sdk, ent) + return ctrl.Result{}, updateControlPlane(ctx, sdk.ControlPlanes, ent) case *configurationv1alpha1.KongService: - return ctrl.Result{}, updateService(ctx, sdk, cl, ent) + return ctrl.Result{}, updateService(ctx, sdk.Services, ent) case *configurationv1alpha1.KongRoute: - return ctrl.Result{}, updateRoute(ctx, sdk, cl, ent) + return ctrl.Result{}, updateRoute(ctx, sdk.Routes, cl, ent) case *configurationv1.KongConsumer: - return ctrl.Result{}, updateConsumer(ctx, sdk, cl, ent) + return ctrl.Result{}, updateConsumer(ctx, sdk.Consumers, cl, ent) case *configurationv1beta1.KongConsumerGroup: - return ctrl.Result{}, updateConsumerGroup(ctx, sdk, cl, ent) + return ctrl.Result{}, updateConsumerGroup(ctx, sdk.ConsumerGroups, cl, ent) case *configurationv1alpha1.KongPluginBinding: return ctrl.Result{}, updatePlugin(ctx, sdk, cl, ent) @@ -174,8 +176,8 @@ func Update[ } func logOpComplete[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ](ctx context.Context, start time.Time, op Op, e TEnt) { s := e.GetKonnectStatus() if s == nil { @@ -186,7 +188,7 @@ func logOpComplete[ Info("operation in Konnect API complete", "op", op, "duration", time.Since(start), - "type", entityTypeName[T](), + "type", constraints.EntityTypeName[T](), "konnect_id", s.GetKonnectID(), ) } @@ -194,17 +196,18 @@ func logOpComplete[ // wrapErrIfKonnectOpFailed checks the response from the Konnect API and returns a uniform // error for all Konnect entities if the operation failed. func wrapErrIfKonnectOpFailed[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ](err error, op Op, e TEnt) error { if err != nil { + entityTypeName := constraints.EntityTypeName[T]() if e == nil { - return fmt.Errorf("failed to %s for %T: %w", - op, e, err, + return fmt.Errorf("failed to %s %s: %w", + op, entityTypeName, err, ) } - return fmt.Errorf("failed to %s for %T %q: %w", - op, client.ObjectKeyFromObject(e), e, err, + return fmt.Errorf("failed to %s %s %s: %w", + op, entityTypeName, client.ObjectKeyFromObject(e), err, ) } return nil diff --git a/controller/konnect/ops_controlplane.go b/controller/konnect/ops/ops_controlplane.go similarity index 76% rename from controller/konnect/ops_controlplane.go rename to controller/konnect/ops/ops_controlplane.go index 7c07a6e24..a848d71c4 100644 --- a/controller/konnect/ops_controlplane.go +++ b/controller/konnect/ops/ops_controlplane.go @@ -1,15 +1,16 @@ -package konnect +package ops import ( "context" "errors" sdkkonnectgo "github.com/Kong/sdk-konnect-go" - "github.com/Kong/sdk-konnect-go/models/components" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" @@ -17,10 +18,10 @@ import ( func createControlPlane( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ControlPlaneSDK, cp *konnectv1alpha1.KonnectGatewayControlPlane, ) error { - resp, err := sdk.ControlPlanes.CreateControlPlane(ctx, cp.Spec.CreateControlPlaneRequest) + resp, err := sdk.CreateControlPlane(ctx, cp.Spec.CreateControlPlaneRequest) // TODO: handle already exists // Can't adopt it as it will cause conflicts between the controller // that created that entity and already manages it, hm @@ -28,7 +29,7 @@ func createControlPlane( if errWrap := wrapErrIfKonnectOpFailed(err, CreateOp, cp); errWrap != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrap.Error(), @@ -42,9 +43,9 @@ func createControlPlane( cp.Status.SetKonnectID(resp.ControlPlane.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", cp.GetGeneration(), ), @@ -58,13 +59,13 @@ func createControlPlane( // It is assumed that the Konnect ControlPlane has a Konnect ID. func deleteControlPlane( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ControlPlaneSDK, cp *konnectv1alpha1.KonnectGatewayControlPlane, ) error { id := cp.GetKonnectStatus().GetKonnectID() - _, err := sdk.ControlPlanes.DeleteControlPlane(ctx, id) + _, err := sdk.DeleteControlPlane(ctx, id) if errWrap := wrapErrIfKonnectOpFailed(err, DeleteOp, cp); errWrap != nil { - var sdkNotFoundError *sdkerrors.NotFoundError + var sdkNotFoundError *sdkkonnecterrs.NotFoundError if errors.As(err, &sdkNotFoundError) { ctrllog.FromContext(ctx). Info("entity not found in Konnect, skipping delete", @@ -72,7 +73,7 @@ func deleteControlPlane( ) return nil } - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrap, &sdkError) { return FailedKonnectOpError[konnectv1alpha1.KonnectGatewayControlPlane]{ Op: DeleteOp, @@ -93,20 +94,20 @@ func deleteControlPlane( // It returns an error if the operation fails. func updateControlPlane( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ControlPlaneSDK, cp *konnectv1alpha1.KonnectGatewayControlPlane, ) error { id := cp.GetKonnectStatus().GetKonnectID() - req := components.UpdateControlPlaneRequest{ + req := sdkkonnectcomp.UpdateControlPlaneRequest{ Name: sdkkonnectgo.String(cp.Spec.Name), Description: cp.Spec.Description, - AuthType: (*components.UpdateControlPlaneRequestAuthType)(cp.Spec.AuthType), + AuthType: (*sdkkonnectcomp.UpdateControlPlaneRequestAuthType)(cp.Spec.AuthType), ProxyUrls: cp.Spec.ProxyUrls, Labels: cp.Spec.Labels, } - resp, err := sdk.ControlPlanes.UpdateControlPlane(ctx, id, req) - var sdkError *sdkerrors.NotFoundError + resp, err := sdk.UpdateControlPlane(ctx, id, req) + var sdkError *sdkkonnecterrs.NotFoundError if errors.As(err, &sdkError) { ctrllog.FromContext(ctx). Info("entity not found in Konnect, trying to recreate", @@ -126,7 +127,7 @@ func updateControlPlane( if errWrap := wrapErrIfKonnectOpFailed(err, UpdateOp, cp); errWrap != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToUpdate", errWrap.Error(), @@ -143,9 +144,9 @@ func updateControlPlane( cp.Status.SetKonnectID(resp.ControlPlane.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", cp.GetGeneration(), ), diff --git a/controller/konnect/ops/ops_controlplane_test.go b/controller/konnect/ops/ops_controlplane_test.go new file mode 100644 index 000000000..afff2cbce --- /dev/null +++ b/controller/konnect/ops/ops_controlplane_test.go @@ -0,0 +1,450 @@ +package ops + +import ( + "context" + "testing" + + sdkkonnectgo "github.com/Kong/sdk-konnect-go" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kong/gateway-operator/controller/konnect/conditions" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func TestCreateControlPlane(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockCPPair func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) + expectedErr bool + assertions func(*testing.T, *konnectv1alpha1.KonnectGatewayControlPlane) + }{ + { + name: "success", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + } + + sdk. + EXPECT(). + CreateControlPlane(ctx, cp.Spec.CreateControlPlaneRequest). + Return( + &sdkkonnectops.CreateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: "12345", + }, + }, + nil, + ) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { + assert.Equal(t, "12345", cp.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) + }, + }, + { + name: "fail", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + } + + sdk. + EXPECT(). + CreateControlPlane(ctx, cp.Spec.CreateControlPlaneRequest). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { + assert.Equal(t, "", cp.Status.GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToCreate", cond.Reason) + assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to create KonnectGatewayControlPlane default/cp-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, cp := tc.mockCPPair() + + err := createControlPlane(ctx, sdk, cp) + t.Cleanup(func() { + assert.True(t, sdk.AssertExpectations(t)) + }) + + tc.assertions(t, cp) + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDeleteControlPlane(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockCPPair func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) + expectedErr bool + assertions func(*testing.T, *konnectv1alpha1.KonnectGatewayControlPlane) + }{ + { + name: "success", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + sdk. + EXPECT(). + DeleteControlPlane(ctx, "12345"). + Return( + &sdkkonnectops.DeleteControlPlaneResponse{ + StatusCode: 200, + }, + nil, + ) + + return sdk, cp + }, + }, + { + name: "fail", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + sdk. + EXPECT(). + DeleteControlPlane(ctx, "12345"). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, cp + }, + expectedErr: true, + }, + { + name: "not found error is ignored and considered a success when trying to delete", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + sdk. + EXPECT(). + DeleteControlPlane(ctx, "12345"). + Return( + nil, + &sdkkonnecterrs.NotFoundError{ + Status: 404, + Detail: "not found", + }, + ) + + return sdk, cp + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, cp := tc.mockCPPair() + + err := deleteControlPlane(ctx, sdk, cp) + + if tc.assertions != nil { + tc.assertions(t, cp) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} + +func TestUpdateControlPlane(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockCPPair func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) + expectedErr bool + assertions func(*testing.T, *konnectv1alpha1.KonnectGatewayControlPlane) + }{ + { + name: "success", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + sdk. + EXPECT(). + UpdateControlPlane(ctx, "12345", + sdkkonnectcomp.UpdateControlPlaneRequest{ + Name: sdkkonnectgo.String(cp.Spec.Name), + Description: cp.Spec.Description, + AuthType: (*sdkkonnectcomp.UpdateControlPlaneRequestAuthType)(cp.Spec.AuthType), + ProxyUrls: cp.Spec.ProxyUrls, + Labels: cp.Spec.Labels, + }, + ). + Return( + &sdkkonnectops.UpdateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: "12345", + }, + }, + nil, + ) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { + assert.Equal(t, "12345", cp.Status.GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + { + name: "fail", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + + sdk. + EXPECT(). + UpdateControlPlane(ctx, "12345", + sdkkonnectcomp.UpdateControlPlaneRequest{ + Name: sdkkonnectgo.String(cp.Spec.Name), + Description: cp.Spec.Description, + AuthType: (*sdkkonnectcomp.UpdateControlPlaneRequestAuthType)(cp.Spec.AuthType), + ProxyUrls: cp.Spec.ProxyUrls, + Labels: cp.Spec.Labels, + }, + ). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { + assert.Equal(t, "12345", cp.Status.GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToUpdate", cond.Reason) + assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to update KonnectGatewayControlPlane default/cp-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + { + name: "when not found then try to create", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectGatewayControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectGatewayControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-1", + Namespace: "default", + }, + Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectGatewayControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + + sdk. + EXPECT(). + UpdateControlPlane(ctx, "12345", + sdkkonnectcomp.UpdateControlPlaneRequest{ + Name: sdkkonnectgo.String(cp.Spec.Name), + Description: cp.Spec.Description, + AuthType: (*sdkkonnectcomp.UpdateControlPlaneRequestAuthType)(cp.Spec.AuthType), + ProxyUrls: cp.Spec.ProxyUrls, + Labels: cp.Spec.Labels, + }, + ). + Return( + nil, + &sdkkonnecterrs.NotFoundError{ + Status: 404, + Detail: "not found", + }, + ) + + sdk. + EXPECT(). + CreateControlPlane(ctx, cp.Spec.CreateControlPlaneRequest). + Return( + &sdkkonnectops.CreateControlPlaneResponse{ + ControlPlane: &sdkkonnectcomp.ControlPlane{ + ID: "12345", + }, + }, + nil, + ) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectGatewayControlPlane) { + assert.Equal(t, "12345", cp.Status.GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, cp := tc.mockCPPair() + + err := updateControlPlane(ctx, sdk, cp) + + if tc.assertions != nil { + tc.assertions(t, cp) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} diff --git a/controller/konnect/ops_kongconsumer.go b/controller/konnect/ops/ops_kongconsumer.go similarity index 82% rename from controller/konnect/ops_kongconsumer.go rename to controller/konnect/ops/ops_kongconsumer.go index cfa628af5..51890d7b3 100644 --- a/controller/konnect/ops_kongconsumer.go +++ b/controller/konnect/ops/ops_kongconsumer.go @@ -1,19 +1,19 @@ -package konnect +package ops import ( "context" "errors" "fmt" - sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" @@ -23,14 +23,14 @@ import ( func createConsumer( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumersSDK, consumer *configurationv1.KongConsumer, ) error { if consumer.GetControlPlaneID() == "" { return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", consumer, client.ObjectKeyFromObject(consumer)) } - resp, err := sdk.Consumers.CreateConsumer(ctx, + resp, err := sdk.CreateConsumer(ctx, consumer.Status.Konnect.ControlPlaneID, kongConsumerToSDKConsumerInput(consumer), ) @@ -41,7 +41,7 @@ func createConsumer( if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, consumer); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -55,9 +55,9 @@ func createConsumer( consumer.Status.Konnect.SetKonnectID(*resp.Consumer.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", consumer.GetGeneration(), ), @@ -72,7 +72,7 @@ func createConsumer( // It returns an error if the KongConsumer does not have a ControlPlaneRef. func updateConsumer( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumersSDK, cl client.Client, consumer *configurationv1.KongConsumer, ) error { @@ -100,8 +100,8 @@ func updateConsumer( ) } - resp, err := sdk.Consumers.UpsertConsumer(ctx, - sdkkonnectgoops.UpsertConsumerRequest{ + resp, err := sdk.UpsertConsumer(ctx, + sdkkonnectops.UpsertConsumerRequest{ ControlPlaneID: cp.Status.ID, ConsumerID: consumer.GetKonnectStatus().GetKonnectID(), Consumer: kongConsumerToSDKConsumerInput(consumer), @@ -114,7 +114,7 @@ func updateConsumer( if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, consumer); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -129,9 +129,9 @@ func updateConsumer( consumer.Status.Konnect.SetControlPlaneID(cp.Status.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", consumer.GetGeneration(), ), @@ -146,14 +146,14 @@ func updateConsumer( // It returns an error if the operation fails. func deleteConsumer( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumersSDK, consumer *configurationv1.KongConsumer, ) error { id := consumer.Status.Konnect.GetKonnectID() - _, err := sdk.Consumers.DeleteConsumer(ctx, consumer.Status.Konnect.ControlPlaneID, id) + _, err := sdk.DeleteConsumer(ctx, consumer.Status.Konnect.ControlPlaneID, id) if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, consumer); errWrapped != nil { // Consumer delete operation returns an SDKError instead of a NotFoundError. - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrapped, &sdkError) { if sdkError.StatusCode == 404 { ctrllog.FromContext(ctx). @@ -178,8 +178,8 @@ func deleteConsumer( func kongConsumerToSDKConsumerInput( consumer *configurationv1.KongConsumer, -) sdkkonnectgocomp.ConsumerInput { - return sdkkonnectgocomp.ConsumerInput{ +) sdkkonnectcomp.ConsumerInput { + return sdkkonnectcomp.ConsumerInput{ CustomID: &consumer.CustomID, Tags: metadata.ExtractTags(consumer), Username: &consumer.Username, diff --git a/controller/konnect/ops_kongconsumergroup.go b/controller/konnect/ops/ops_kongconsumergroup.go similarity index 82% rename from controller/konnect/ops_kongconsumergroup.go rename to controller/konnect/ops/ops_kongconsumergroup.go index fd5a184b8..979802007 100644 --- a/controller/konnect/ops_kongconsumergroup.go +++ b/controller/konnect/ops/ops_kongconsumergroup.go @@ -1,19 +1,19 @@ -package konnect +package ops import ( "context" "errors" "fmt" - sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1" @@ -23,14 +23,14 @@ import ( func createConsumerGroup( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumerGroupSDK, group *configurationv1beta1.KongConsumerGroup, ) error { if group.GetControlPlaneID() == "" { return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", group, client.ObjectKeyFromObject(group)) } - resp, err := sdk.ConsumerGroups.CreateConsumerGroup(ctx, + resp, err := sdk.CreateConsumerGroup(ctx, group.Status.Konnect.ControlPlaneID, kongConsumerGroupToSDKConsumerGroupInput(group), ) @@ -41,7 +41,7 @@ func createConsumerGroup( if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, group); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -55,9 +55,9 @@ func createConsumerGroup( group.Status.Konnect.SetKonnectID(*resp.ConsumerGroup.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", group.GetGeneration(), ), @@ -72,7 +72,7 @@ func createConsumerGroup( // It returns an error if the KongConsumerGroup does not have a ControlPlaneRef. func updateConsumerGroup( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumerGroupSDK, cl client.Client, group *configurationv1beta1.KongConsumerGroup, ) error { @@ -100,8 +100,8 @@ func updateConsumerGroup( ) } - resp, err := sdk.ConsumerGroups.UpsertConsumerGroup(ctx, - sdkkonnectgoops.UpsertConsumerGroupRequest{ + resp, err := sdk.UpsertConsumerGroup(ctx, + sdkkonnectops.UpsertConsumerGroupRequest{ ControlPlaneID: cp.Status.ID, ConsumerGroupID: group.GetKonnectStatus().GetKonnectID(), ConsumerGroup: kongConsumerGroupToSDKConsumerGroupInput(group), @@ -114,7 +114,7 @@ func updateConsumerGroup( if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, group); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -129,9 +129,9 @@ func updateConsumerGroup( group.Status.Konnect.SetControlPlaneID(cp.Status.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", group.GetGeneration(), ), @@ -146,14 +146,14 @@ func updateConsumerGroup( // It returns an error if the operation fails. func deleteConsumerGroup( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ConsumerGroupSDK, consumer *configurationv1beta1.KongConsumerGroup, ) error { id := consumer.Status.Konnect.GetKonnectID() - _, err := sdk.ConsumerGroups.DeleteConsumerGroup(ctx, consumer.Status.Konnect.ControlPlaneID, id) + _, err := sdk.DeleteConsumerGroup(ctx, consumer.Status.Konnect.ControlPlaneID, id) if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, consumer); errWrapped != nil { // Consumer delete operation returns an SDKError instead of a NotFoundError. - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrapped, &sdkError) { if sdkError.StatusCode == 404 { ctrllog.FromContext(ctx). @@ -178,8 +178,8 @@ func deleteConsumerGroup( func kongConsumerGroupToSDKConsumerGroupInput( group *configurationv1beta1.KongConsumerGroup, -) sdkkonnectgocomp.ConsumerGroupInput { - return sdkkonnectgocomp.ConsumerGroupInput{ +) sdkkonnectcomp.ConsumerGroupInput { + return sdkkonnectcomp.ConsumerGroupInput{ Tags: metadata.ExtractTags(group), Name: group.Spec.Name, } diff --git a/controller/konnect/ops_kongpluginbinding.go b/controller/konnect/ops/ops_kongpluginbinding.go similarity index 90% rename from controller/konnect/ops_kongpluginbinding.go rename to controller/konnect/ops/ops_kongpluginbinding.go index 556d03fb8..d04c6faab 100644 --- a/controller/konnect/ops_kongpluginbinding.go +++ b/controller/konnect/ops/ops_kongpluginbinding.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -7,15 +7,16 @@ import ( "fmt" sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" "github.com/samber/lo" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" @@ -54,7 +55,7 @@ func createPlugin( if errWrapped := wrapErrIfKonnectOpFailed[configurationv1alpha1.KongPluginBinding](err, CreateOp, pluginBinding); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -68,9 +69,9 @@ func createPlugin( pluginBinding.SetKonnectID(*resp.Plugin.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", pluginBinding.GetGeneration(), ), @@ -121,7 +122,7 @@ func updatePlugin( } resp, err := sdk.Plugins.UpsertPlugin(ctx, - sdkkonnectgoops.UpsertPluginRequest{ + sdkkonnectops.UpsertPluginRequest{ ControlPlaneID: controlPlaneID, PluginID: pb.GetKonnectID(), Plugin: *pluginInput, @@ -134,7 +135,7 @@ func updatePlugin( if errWrapped := wrapErrIfKonnectOpFailed[configurationv1alpha1.KongPluginBinding](err, UpdateOp, pb); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -149,9 +150,9 @@ func updatePlugin( pb.Status.Konnect.SetControlPlaneID(cp.Status.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", pb.GetGeneration(), ), @@ -173,7 +174,7 @@ func deletePlugin( _, err := sdk.Plugins.DeletePlugin(ctx, pb.GetControlPlaneID(), id) if errWrapped := wrapErrIfKonnectOpFailed[configurationv1alpha1.KongPluginBinding](err, DeleteOp, pb); errWrapped != nil { // plugin delete operation returns an SDKError instead of a NotFoundError. - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrapped, &sdkError) && sdkError.StatusCode == 404 { ctrllog.FromContext(ctx). Info("entity not found in Konnect, skipping delete", @@ -195,7 +196,7 @@ func deletePlugin( // ----------------------------------------------------------------------------- // getPluginInput returns the SDK PluginInput for the KongPluginBinding. -func getPluginInput(ctx context.Context, cl client.Client, pluginBinding *configurationv1alpha1.KongPluginBinding) (*sdkkonnectgocomp.PluginInput, error) { +func getPluginInput(ctx context.Context, cl client.Client, pluginBinding *configurationv1alpha1.KongPluginBinding) (*sdkkonnectcomp.PluginInput, error) { plugin, err := getReferencedPlugin(ctx, cl, pluginBinding) if err != nil { return nil, err @@ -256,7 +257,7 @@ func getReferencedPlugin(ctx context.Context, cl client.Client, pluginBinding *c func kongPluginBindingToSDKPluginInput( plugin *configurationv1.KongPlugin, targets []client.Object, -) (*sdkkonnectgocomp.PluginInput, error) { +) (*sdkkonnectcomp.PluginInput, error) { if len(targets) == 0 { return nil, fmt.Errorf("no targets found for KongPluginBinding %s", client.ObjectKeyFromObject(plugin)) } @@ -266,7 +267,7 @@ func kongPluginBindingToSDKPluginInput( return nil, err } - pluginInput := &sdkkonnectgocomp.PluginInput{ + pluginInput := &sdkkonnectcomp.PluginInput{ Name: lo.ToPtr(plugin.PluginName), Config: pluginConfig, Enabled: lo.ToPtr(!plugin.Disabled), @@ -281,7 +282,7 @@ func kongPluginBindingToSDKPluginInput( if id == "" { return nil, fmt.Errorf("KongService %s is not configured in Konnect yet", client.ObjectKeyFromObject(t)) } - pluginInput.Service = &sdkkonnectgocomp.PluginService{ + pluginInput.Service = &sdkkonnectcomp.PluginService{ ID: lo.ToPtr(t.GetKonnectStatus().ID), } // TODO(mlavacca): add support for KongRoute diff --git a/controller/konnect/ops_kongroute.go b/controller/konnect/ops/ops_kongroute.go similarity index 84% rename from controller/konnect/ops_kongroute.go rename to controller/konnect/ops/ops_kongroute.go index f6e5aff99..bf857a53a 100644 --- a/controller/konnect/ops_kongroute.go +++ b/controller/konnect/ops/ops_kongroute.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -6,14 +6,15 @@ import ( "fmt" sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" @@ -22,14 +23,14 @@ import ( func createRoute( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk RoutesSDK, route *configurationv1alpha1.KongRoute, ) error { if route.GetControlPlaneID() == "" { return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", route, client.ObjectKeyFromObject(route)) } - resp, err := sdk.Routes.CreateRoute(ctx, route.Status.Konnect.ControlPlaneID, kongRouteToSDKRouteInput(route)) + resp, err := sdk.CreateRoute(ctx, route.Status.Konnect.ControlPlaneID, kongRouteToSDKRouteInput(route)) // TODO: handle already exists // Can't adopt it as it will cause conflicts between the controller @@ -37,7 +38,7 @@ func createRoute( if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, route); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -51,9 +52,9 @@ func createRoute( route.Status.Konnect.SetKonnectID(*resp.Route.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", route.GetGeneration(), ), @@ -69,7 +70,8 @@ func createRoute( // if the operation fails. func updateRoute( ctx context.Context, - sdk *sdkkonnectgo.SDK, + // sdk *sdkkonnectgo.SDK, + sdk RoutesSDK, cl client.Client, route *configurationv1alpha1.KongRoute, ) error { @@ -114,7 +116,8 @@ func updateRoute( ) } - resp, err := sdk.Routes.UpsertRoute(ctx, sdkkonnectgoops.UpsertRouteRequest{ + resp, err := sdk.UpsertRoute(ctx, sdkkonnectops.UpsertRouteRequest{ + // resp, err := sdk.UpsertRoute(ctx, sdkkonnectops.UpsertRouteRequest{ ControlPlaneID: cp.Status.ID, RouteID: route.Status.Konnect.ID, Route: kongRouteToSDKRouteInput(route), @@ -127,7 +130,7 @@ func updateRoute( if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, route); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -142,9 +145,9 @@ func updateRoute( route.Status.Konnect.SetControlPlaneID(cp.Status.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", route.GetGeneration(), ), @@ -159,14 +162,14 @@ func updateRoute( // It returns an error if the operation fails. func deleteRoute( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk RoutesSDK, route *configurationv1alpha1.KongRoute, ) error { id := route.GetKonnectStatus().GetKonnectID() - _, err := sdk.Routes.DeleteRoute(ctx, route.Status.Konnect.ControlPlaneID, id) + _, err := sdk.DeleteRoute(ctx, route.Status.Konnect.ControlPlaneID, id) if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, route); errWrapped != nil { // Service delete operation returns an SDKError instead of a NotFoundError. - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrapped, &sdkError) { if sdkError.StatusCode == 404 { ctrllog.FromContext(ctx). @@ -191,8 +194,8 @@ func deleteRoute( func kongRouteToSDKRouteInput( route *configurationv1alpha1.KongRoute, -) sdkkonnectgocomp.RouteInput { - return sdkkonnectgocomp.RouteInput{ +) sdkkonnectcomp.RouteInput { + return sdkkonnectcomp.RouteInput{ Destinations: route.Spec.KongRouteAPISpec.Destinations, Headers: route.Spec.KongRouteAPISpec.Headers, Hosts: route.Spec.KongRouteAPISpec.Hosts, @@ -210,7 +213,7 @@ func kongRouteToSDKRouteInput( Sources: route.Spec.KongRouteAPISpec.Sources, StripPath: route.Spec.KongRouteAPISpec.StripPath, Tags: route.Spec.KongRouteAPISpec.Tags, - Service: &sdkkonnectgocomp.RouteService{ + Service: &sdkkonnectcomp.RouteService{ ID: sdkkonnectgo.String(route.Status.Konnect.ServiceID), }, } diff --git a/controller/konnect/ops_kongservice.go b/controller/konnect/ops/ops_kongservice.go similarity index 67% rename from controller/konnect/ops_kongservice.go rename to controller/konnect/ops/ops_kongservice.go index 5aa872e09..44559029c 100644 --- a/controller/konnect/ops_kongservice.go +++ b/controller/konnect/ops/ops_kongservice.go @@ -1,35 +1,36 @@ -package konnect +package ops import ( "context" "errors" "fmt" - sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" - "github.com/Kong/sdk-konnect-go/models/sdkerrors" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" - konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) func createService( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ServicesSDK, svc *configurationv1alpha1.KongService, ) error { if svc.GetControlPlaneID() == "" { - return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", svc, client.ObjectKeyFromObject(svc)) + return fmt.Errorf( + "can't create %T %s without a Konnect ControlPlane ID", + svc, client.ObjectKeyFromObject(svc), + ) } - resp, err := sdk.Services.CreateService(ctx, + resp, err := sdk.CreateService(ctx, svc.Status.Konnect.ControlPlaneID, kongServiceToSDKServiceInput(svc), ) @@ -40,7 +41,7 @@ func createService( if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, svc); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -54,9 +55,9 @@ func createService( svc.Status.Konnect.SetKonnectID(*resp.Service.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", svc.GetGeneration(), ), @@ -72,38 +73,20 @@ func createService( // if the operation fails. func updateService( ctx context.Context, - sdk *sdkkonnectgo.SDK, - cl client.Client, + sdk ServicesSDK, svc *configurationv1alpha1.KongService, ) error { - if svc.Spec.ControlPlaneRef == nil { - return fmt.Errorf("can't update %T without a ControlPlaneRef", svc) - } - - // TODO(pmalek) handle other types of CP ref - // TODO(pmalek) handle cross namespace refs - nnCP := types.NamespacedName{ - Namespace: svc.Namespace, - Name: svc.Spec.ControlPlaneRef.KonnectNamespacedRef.Name, - } - var cp konnectv1alpha1.KonnectGatewayControlPlane - if err := cl.Get(ctx, nnCP, &cp); err != nil { - return fmt.Errorf("failed to get KonnectGatewayControlPlane %s: for %T %s: %w", - nnCP, svc, client.ObjectKeyFromObject(svc), err, - ) - } - - if cp.Status.ID == "" { - return fmt.Errorf( - "can't update %T when referenced KonnectGatewayControlPlane %s does not have the Konnect ID", - svc, nnCP, + if svc.GetControlPlaneID() == "" { + return fmt.Errorf("can't update %T %s without a Konnect ControlPlane ID", + svc, client.ObjectKeyFromObject(svc), ) } - resp, err := sdk.Services.UpsertService(ctx, - sdkkonnectgoops.UpsertServiceRequest{ - ControlPlaneID: cp.Status.ID, - ServiceID: svc.GetKonnectStatus().GetKonnectID(), + id := svc.GetKonnectStatus().GetKonnectID() + resp, err := sdk.UpsertService(ctx, + sdkkonnectops.UpsertServiceRequest{ + ControlPlaneID: svc.GetControlPlaneID(), + ServiceID: id, Service: kongServiceToSDKServiceInput(svc), }, ) @@ -112,11 +95,33 @@ func updateService( // Can't adopt it as it will cause conflicts between the controller // that created that entity and already manages it, hm if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, svc); errWrapped != nil { + // Service update operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnecterrs.SDKError + if errors.As(errWrapped, &sdkError) { + switch sdkError.StatusCode { + case 404: + if err := createService(ctx, sdk, svc); err != nil { + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: UpdateOp, + Err: err, + } + } + // Create succeeded, createService sets the status so no need to do this here. + + return nil + default: + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: UpdateOp, + Err: sdkError, + } + } + } + k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, - "FailedToCreate", + "FailedToUpdate", errWrapped.Error(), svc.GetGeneration(), ), @@ -126,12 +131,12 @@ func updateService( } svc.Status.Konnect.SetKonnectID(*resp.Service.ID) - svc.Status.Konnect.SetControlPlaneID(cp.Status.ID) + svc.Status.Konnect.SetControlPlaneID(svc.GetControlPlaneID()) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", svc.GetGeneration(), ), @@ -146,14 +151,14 @@ func updateService( // It returns an error if the operation fails. func deleteService( ctx context.Context, - sdk *sdkkonnectgo.SDK, + sdk ServicesSDK, svc *configurationv1alpha1.KongService, ) error { id := svc.GetKonnectStatus().GetKonnectID() - _, err := sdk.Services.DeleteService(ctx, svc.Status.Konnect.ControlPlaneID, id) + _, err := sdk.DeleteService(ctx, svc.Status.Konnect.ControlPlaneID, id) if errWrapped := wrapErrIfKonnectOpFailed(err, DeleteOp, svc); errWrapped != nil { // Service delete operation returns an SDKError instead of a NotFoundError. - var sdkError *sdkerrors.SDKError + var sdkError *sdkkonnecterrs.SDKError if errors.As(errWrapped, &sdkError) { switch sdkError.StatusCode { case 404: @@ -180,8 +185,8 @@ func deleteService( func kongServiceToSDKServiceInput( svc *configurationv1alpha1.KongService, -) sdkkonnectgocomp.ServiceInput { - return sdkkonnectgocomp.ServiceInput{ +) sdkkonnectcomp.ServiceInput { + return sdkkonnectcomp.ServiceInput{ URL: svc.Spec.KongServiceAPISpec.URL, ConnectTimeout: svc.Spec.KongServiceAPISpec.ConnectTimeout, Enabled: svc.Spec.KongServiceAPISpec.Enabled, diff --git a/controller/konnect/ops/ops_kongservice_test.go b/controller/konnect/ops/ops_kongservice_test.go new file mode 100644 index 000000000..fa0e6421e --- /dev/null +++ b/controller/konnect/ops/ops_kongservice_test.go @@ -0,0 +1,501 @@ +package ops + +import ( + "context" + "testing" + + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnecterrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kong/gateway-operator/controller/konnect/conditions" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func TestCreateKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "123456789", + }, + }, + } + + sdk. + EXPECT(). + CreateService(ctx, "123456789", kongServiceToSDKServiceInput(svc)). + Return( + &sdkkonnectops.CreateServiceResponse{ + Service: &sdkkonnectcomp.Service{ + ID: lo.ToPtr("12345"), + Host: "example.com", + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "12345", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KongService") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + }, + }, + { + name: "fail - no control plane ID in status returns an error and does not create the Service in Konnect", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + } + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + // TODO: we should probably set a condition when the control plane ID is missing in the status. + }, + expectedErr: true, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "123456789", + }, + }, + } + + sdk. + EXPECT(). + CreateService(ctx, "123456789", kongServiceToSDKServiceInput(svc)). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToCreate", cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to create KongService default/svc-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := createService(ctx, sdk, svc) + t.Cleanup(func() { + assert.True(t, sdk.AssertExpectations(t)) + }) + + tc.assertions(t, svc) + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDeleteKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + &sdkkonnectops.DeleteServiceResponse{ + StatusCode: 200, + }, + nil, + ) + + return sdk, svc + }, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, svc + }, + expectedErr: true, + }, + { + name: "not found error is ignored and considered a success when trying to delete", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + nil, + &sdkkonnecterrs.SDKError{ + Message: "not found", + StatusCode: 404, + }, + ) + + return sdk, svc + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := deleteService(ctx, sdk, svc) + + if tc.assertions != nil { + tc.assertions(t, svc) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} + +func TestUpdateKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + &sdkkonnectops.UpsertServiceResponse{ + StatusCode: 200, + Service: &sdkkonnectcomp.Service{ + ID: lo.ToPtr("123456789"), + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "123456789", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + nil, + &sdkkonnecterrs.BadRequestError{ + Status: 400, + Title: "bad request", + }, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + // TODO: When we fail to update a KongService, do we want to clear + // the Konnect ID from the status? Probably not. + // assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToUpdate", cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to update KongService default/svc-1: {"status":400,"title":"bad request","instance":"","detail":"","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + { + name: "when not found then try to create", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + nil, + &sdkkonnecterrs.SDKError{ + StatusCode: 404, + Message: "not found", + }, + ) + + sdk. + EXPECT(). + CreateService(ctx, "12345", kongServiceToSDKServiceInput(svc)). + Return( + &sdkkonnectops.CreateServiceResponse{ + Service: &sdkkonnectcomp.Service{ + ID: lo.ToPtr("123456789"), + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "123456789", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectGatewayControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := updateService(ctx, sdk, svc) + + if tc.assertions != nil { + tc.assertions(t, svc) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index 8720c2953..fcd16b455 100644 --- a/controller/konnect/reconciler_generic.go +++ b/controller/konnect/reconciler_generic.go @@ -16,6 +16,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" + "github.com/kong/gateway-operator/controller/konnect/conditions" + "github.com/kong/gateway-operator/controller/konnect/constraints" + "github.com/kong/gateway-operator/controller/konnect/ops" "github.com/kong/gateway-operator/controller/pkg/log" "github.com/kong/gateway-operator/pkg/consts" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" @@ -35,7 +38,7 @@ const ( // KonnectEntityReconciler reconciles a Konnect entities. // It uses the generic type constraints to constrain the supported types. -type KonnectEntityReconciler[T SupportedKonnectEntityType, TEnt EntityType[T]] struct { +type KonnectEntityReconciler[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]] struct { sdkFactory SDKFactory DevelopmentMode bool Client client.Client @@ -44,12 +47,12 @@ type KonnectEntityReconciler[T SupportedKonnectEntityType, TEnt EntityType[T]] s // KonnectEntityReconcilerOption is a functional option for the KonnectEntityReconciler. type KonnectEntityReconcilerOption[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ] func(*KonnectEntityReconciler[T, TEnt]) // WithKonnectEntitySyncPeriod sets the sync period for the reconciler. -func WithKonnectEntitySyncPeriod[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func WithKonnectEntitySyncPeriod[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( d time.Duration, ) KonnectEntityReconcilerOption[T, TEnt] { return func(r *KonnectEntityReconciler[T, TEnt]) { @@ -60,8 +63,8 @@ func WithKonnectEntitySyncPeriod[T SupportedKonnectEntityType, TEnt EntityType[T // NewKonnectEntityReconciler returns a new KonnectEntityReconciler for the given // Konnect entity type. func NewKonnectEntityReconciler[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ]( sdkFactory SDKFactory, developmentMode bool, @@ -92,7 +95,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) SetupWithManager(mgr ctrl.Manager) er e T ent = TEnt(&e) b = ctrl.NewControllerManagedBy(mgr). - Named(entityTypeName[T]()). + Named(constraints.EntityTypeName[T]()). WithOptions(controller.Options{ MaxConcurrentReconciles: MaxConcurrentReconciles, }) @@ -116,7 +119,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( ctx context.Context, req ctrl.Request, ) (ctrl.Result, error) { var ( - entityTypeName = entityTypeName[T]() + entityTypeName = constraints.EntityTypeName[T]() logger = log.GetLogger(ctx, entityTypeName, r.DevelopmentMode) ) @@ -185,9 +188,9 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( if k8serrors.IsNotFound(err) { if res, err := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefReasonRefNotFound, fmt.Sprintf("Referenced KonnectAPIAuthConfiguration %s not found", apiAuthRef), ); err != nil || res.Requeue { return ctrl.Result{}, err @@ -198,9 +201,9 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( if res, err := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationResolvedRefReasonRefInvalid, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefReasonRefInvalid, fmt.Sprintf("KonnectAPIAuthConfiguration reference %s is invalid: %v", apiAuthRef, err), ); err != nil || res.Requeue { return ctrl.Result{}, err @@ -210,15 +213,15 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } // Update the status if the reference is resolved and it's not as expected. - if cond, present := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationResolvedRefConditionType, ent); !present || + if cond, present := k8sutils.GetCondition(conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, ent); !present || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != ent.GetGeneration() || - cond.Reason != KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef { + cond.Reason != conditions.KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef { if res, err := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityAPIAuthConfigurationResolvedRefConditionType, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, metav1.ConditionTrue, - KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefReasonResolvedRef, fmt.Sprintf("KonnectAPIAuthConfiguration reference %s is resolved", apiAuthRef), ); err != nil || res.Requeue { return res, err @@ -227,17 +230,17 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } // Check if the referenced APIAuthConfiguration is valid. - if cond, present := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !present || + if cond, present := k8sutils.GetCondition(conditions.KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !present || cond.Status != metav1.ConditionTrue || - cond.Reason != KonnectEntityAPIAuthConfigurationReasonValid { + cond.Reason != conditions.KonnectEntityAPIAuthConfigurationReasonValid { // If it's invalid then set the "APIAuthValid" status condition on // the entity to False with "Invalid" reason. if res, err := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationReasonInvalid, + conditions.KonnectEntityAPIAuthConfigurationReasonInvalid, conditionMessageReferenceKonnectAPIAuthConfigurationInvalid(apiAuthRef), ); err != nil || res.Requeue { return res, err @@ -249,17 +252,17 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( // If the referenced APIAuthConfiguration is valid, set the "APIAuthValid" // condition to True with "Valid" reason. // Only perform the update if the condition is not as expected. - if cond, present := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, ent); !present || + if cond, present := k8sutils.GetCondition(conditions.KonnectEntityAPIAuthConfigurationValidConditionType, ent); !present || cond.Status != metav1.ConditionTrue || - cond.Reason != KonnectEntityAPIAuthConfigurationReasonValid || + cond.Reason != conditions.KonnectEntityAPIAuthConfigurationReasonValid || cond.ObservedGeneration != ent.GetGeneration() || cond.Message != conditionMessageReferenceKonnectAPIAuthConfigurationValid(apiAuthRef) { if res, err := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionTrue, - KonnectEntityAPIAuthConfigurationReasonValid, + conditions.KonnectEntityAPIAuthConfigurationReasonValid, conditionMessageReferenceKonnectAPIAuthConfigurationValid(apiAuthRef), ); err != nil || res.Requeue { return res, err @@ -271,9 +274,9 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( if err != nil { if res, errStatus := updateStatusWithCondition( ctx, r.Client, &apiAuth, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationReasonInvalid, + conditions.KonnectEntityAPIAuthConfigurationReasonInvalid, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -303,12 +306,12 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( } if controllerutil.RemoveFinalizer(ent, KonnectCleanupFinalizer) { - if err := Delete[T, TEnt](ctx, sdk, r.Client, ent); err != nil { + if err := ops.Delete[T, TEnt](ctx, sdk, r.Client, ent); err != nil { if res, errStatus := updateStatusWithCondition( ctx, r.Client, ent, - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, - KonnectEntityProgrammedReasonKonnectAPIOpFailed, + conditions.KonnectEntityProgrammedReasonKonnectAPIOpFailed, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -333,7 +336,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( // We should look at the "expectations" for this: // https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/controller_utils.go if status := ent.GetKonnectStatus(); status == nil || status.GetKonnectID() == "" { - _, err := Create[T, TEnt](ctx, sdk, r.Client, ent) + _, err := ops.Create[T, TEnt](ctx, sdk, r.Client, ent) if err != nil { // TODO(pmalek): this is actually not 100% error prone because when status // update fails we don't store the Konnect ID and hence the reconciler @@ -345,8 +348,9 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( return ctrl.Result{}, fmt.Errorf("failed to update status after creating object: %w", err) } - return ctrl.Result{}, FailedKonnectOpError[T]{ - Op: CreateOp, + return ctrl.Result{}, ops.FailedKonnectOpError[T]{ + Op: ops.CreateOp, + Err: err, } } @@ -373,7 +377,7 @@ func (r *KonnectEntityReconciler[T, TEnt]) Reconcile( return ctrl.Result{}, nil } - if res, err := Update[T, TEnt](ctx, sdk, r.SyncPeriod, r.Client, ent); err != nil { + if res, err := ops.Update[T, TEnt](ctx, sdk, r.SyncPeriod, r.Client, ent); err != nil { return ctrl.Result{}, fmt.Errorf("failed to update object: %w", err) } else if res.Requeue || res.RequeueAfter > 0 { return res, nil @@ -431,7 +435,7 @@ func updateStatusWithCondition[T interface { } return ctrl.Result{}, fmt.Errorf( "failed to update status with %s condition: %w", - KonnectEntityAPIAuthConfigurationResolvedRefConditionType, err, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, err, ) } @@ -478,7 +482,7 @@ func getCPAuthRefForRef( }, nil } -func getAPIAuthRefNN[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func getAPIAuthRefNN[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( ctx context.Context, cl client.Client, ent TEnt, @@ -515,7 +519,7 @@ func getAPIAuthRefNN[T SupportedKonnectEntityType, TEnt EntityType[T]]( return getCPAuthRefForRef(ctx, cl, cpRef, ent.GetNamespace()) } - if ref, ok := any(ent).(EntityWithKonnectAPIAuthConfigurationRef); ok { + if ref, ok := any(ent).(constraints.EntityWithKonnectAPIAuthConfigurationRef); ok { return types.NamespacedName{ Name: ref.GetKonnectAPIAuthConfigurationRef().Name, // TODO(pmalek): enable if cross namespace refs are allowed @@ -529,7 +533,7 @@ func getAPIAuthRefNN[T SupportedKonnectEntityType, TEnt EntityType[T]]( ) } -func getServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func getServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( e TEnt, ) mo.Option[configurationv1alpha1.ServiceRef] { switch e := any(e).(type) { @@ -551,7 +555,7 @@ func getServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( // handleKongServiceRef handles the ServiceRef for the given entity. // It sets the owner reference to the referenced KongService and updates the // status of the entity based on the referenced KongService status. -func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func handleKongServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( ctx context.Context, cl client.Client, ent TEnt, @@ -572,9 +576,9 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if err := cl.Get(ctx, nn, &svc); err != nil { if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - KongServiceRefValidConditionType, + conditions.KongServiceRefValidConditionType, metav1.ConditionFalse, - KongServiceRefReasonInvalid, + conditions.KongServiceRefReasonInvalid, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -591,14 +595,14 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( } } - cond, ok := k8sutils.GetCondition(KonnectEntityProgrammedConditionType, &svc) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, &svc) if !ok || cond.Status != metav1.ConditionTrue { ent.SetKonnectID("") if res, err := updateStatusWithCondition( ctx, cl, ent, - KongServiceRefValidConditionType, + conditions.KongServiceRefValidConditionType, metav1.ConditionFalse, - KongServiceRefReasonInvalid, + conditions.KongServiceRefReasonInvalid, fmt.Sprintf("Referenced KongService %s is not programmed yet", nn), ); err != nil || res.Requeue { return ctrl.Result{}, err @@ -629,9 +633,9 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - KongServiceRefValidConditionType, + conditions.KongServiceRefValidConditionType, metav1.ConditionTrue, - KongServiceRefReasonValid, + conditions.KongServiceRefReasonValid, fmt.Sprintf("Referenced KongService %s programmed", nn), ); errStatus != nil || res.Requeue { return res, errStatus @@ -648,9 +652,9 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if err != nil { if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionFalse, - ControlPlaneRefReasonInvalid, + conditions.ControlPlaneRefReasonInvalid, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -664,13 +668,13 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( return ctrl.Result{}, err } - cond, ok = k8sutils.GetCondition(KonnectEntityProgrammedConditionType, cp) + cond, ok = k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() { if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionFalse, - ControlPlaneRefReasonInvalid, + conditions.ControlPlaneRefReasonInvalid, fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn), ); errStatus != nil || res.Requeue { return res, errStatus @@ -688,9 +692,9 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionTrue, - ControlPlaneRefReasonValid, + conditions.ControlPlaneRefReasonValid, fmt.Sprintf("Referenced ControlPlane %s is programmed", nn), ); errStatus != nil || res.Requeue { return res, errStatus @@ -703,7 +707,7 @@ func handleKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( return ctrl.Result{}, nil } -func getControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func getControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( e TEnt, ) mo.Option[configurationv1alpha1.ControlPlaneRef] { switch e := any(e).(type) { @@ -737,7 +741,7 @@ func getControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( // handleControlPlaneRef handles the ControlPlaneRef for the given entity. // It sets the owner reference to the referenced ControlPlane and updates the // status of the entity based on the referenced ControlPlane status. -func handleControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func handleControlPlaneRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( ctx context.Context, cl client.Client, ent TEnt, @@ -758,9 +762,9 @@ func handleControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if err := cl.Get(ctx, nn, &cp); err != nil { if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionFalse, - ControlPlaneRefReasonInvalid, + conditions.ControlPlaneRefReasonInvalid, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -774,13 +778,13 @@ func handleControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( return ctrl.Result{}, err } - cond, ok := k8sutils.GetCondition(KonnectEntityProgrammedConditionType, &cp) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, &cp) if !ok || cond.Status != metav1.ConditionTrue || cond.ObservedGeneration != cp.GetGeneration() { if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionFalse, - ControlPlaneRefReasonInvalid, + conditions.ControlPlaneRefReasonInvalid, fmt.Sprintf("Referenced ControlPlane %s is not programmed yet", nn), ); errStatus != nil || res.Requeue { return res, errStatus @@ -809,9 +813,9 @@ func handleControlPlaneRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( if res, errStatus := updateStatusWithCondition( ctx, cl, ent, - ControlPlaneRefValidConditionType, + conditions.ControlPlaneRefValidConditionType, metav1.ConditionTrue, - ControlPlaneRefReasonValid, + conditions.ControlPlaneRefReasonValid, fmt.Sprintf("Referenced ControlPlane %s is programmed", nn), ); errStatus != nil || res.Requeue { return res, errStatus diff --git a/controller/konnect/reconciler_generic_test.go b/controller/konnect/reconciler_generic_test.go index 39655d04e..92ac0af91 100644 --- a/controller/konnect/reconciler_generic_test.go +++ b/controller/konnect/reconciler_generic_test.go @@ -8,6 +8,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/kong/gateway-operator/controller/konnect/constraints" "github.com/kong/gateway-operator/modules/manager/scheme" configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" @@ -29,8 +30,8 @@ func TestNewKonnectEntityReconciler(t *testing.T) { } func testNewKonnectEntityReconciler[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ]( t *testing.T, ent T, diff --git a/controller/konnect/reconciler_konnectapiauth.go b/controller/konnect/reconciler_konnectapiauth.go index 07b84bf6d..8e8823044 100644 --- a/controller/konnect/reconciler_konnectapiauth.go +++ b/controller/konnect/reconciler_konnectapiauth.go @@ -7,7 +7,7 @@ import ( "strings" "time" - sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnectops "github.com/Kong/sdk-konnect-go/models/operations" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -18,6 +18,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" + "github.com/kong/gateway-operator/controller/konnect/conditions" "github.com/kong/gateway-operator/controller/pkg/log" k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" @@ -121,9 +122,9 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( if err != nil { if res, errStatus := updateStatusWithCondition( ctx, r.Client, &apiAuth, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationReasonInvalid, + conditions.KonnectEntityAPIAuthConfigurationReasonInvalid, err.Error(), ); errStatus != nil || res.Requeue { return res, errStatus @@ -149,12 +150,12 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( // NOTE: This is needed because currently the SDK only lists the prod global API as supported: // https://github.com/Kong/sdk-konnect-go/blob/999d9a987e1aa7d2e09ac11b1450f4563adf21ea/models/operations/getorganizationsme.go#L10-L12 - respOrg, err := sdk.Me.GetOrganizationsMe(ctx, sdkkonnectgoops.WithServerURL("https://"+apiAuth.Spec.ServerURL)) + respOrg, err := sdk.Me.GetOrganizationsMe(ctx, sdkkonnectops.WithServerURL("https://"+apiAuth.Spec.ServerURL)) if err != nil { logger.Error(err, "failed to get organization info from Konnect") - if cond, ok := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !ok || + if cond, ok := k8sutils.GetCondition(conditions.KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !ok || cond.Status != metav1.ConditionFalse || - cond.Reason != KonnectEntityAPIAuthConfigurationReasonInvalid || + cond.Reason != conditions.KonnectEntityAPIAuthConfigurationReasonInvalid || cond.ObservedGeneration != apiAuth.GetGeneration() || apiAuth.Status.OrganizationID != "" || apiAuth.Status.ServerURL != apiAuth.Spec.ServerURL { @@ -164,9 +165,9 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( res, errUpdate := updateStatusWithCondition( ctx, r.Client, &apiAuth, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionFalse, - KonnectEntityAPIAuthConfigurationReasonInvalid, + conditions.KonnectEntityAPIAuthConfigurationReasonInvalid, err.Error(), ) if errUpdate != nil || res.Requeue { @@ -189,10 +190,10 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( } condMessage = fmt.Sprintf("Token from Secret %s is valid", nn) } - if cond, ok := k8sutils.GetCondition(KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !ok || + if cond, ok := k8sutils.GetCondition(conditions.KonnectEntityAPIAuthConfigurationValidConditionType, &apiAuth); !ok || cond.Status != metav1.ConditionTrue || cond.Message != condMessage || - cond.Reason != KonnectEntityAPIAuthConfigurationReasonValid || + cond.Reason != conditions.KonnectEntityAPIAuthConfigurationReasonValid || cond.ObservedGeneration != apiAuth.GetGeneration() || apiAuth.Status.OrganizationID != *respOrg.MeOrganization.ID || apiAuth.Status.ServerURL != apiAuth.Spec.ServerURL { @@ -202,9 +203,9 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( res, err := updateStatusWithCondition( ctx, r.Client, &apiAuth, - KonnectEntityAPIAuthConfigurationValidConditionType, + conditions.KonnectEntityAPIAuthConfigurationValidConditionType, metav1.ConditionTrue, - KonnectEntityAPIAuthConfigurationReasonValid, + conditions.KonnectEntityAPIAuthConfigurationReasonValid, condMessage, ) if err != nil || res.Requeue { diff --git a/controller/konnect/sdkfactory.go b/controller/konnect/sdkfactory.go index bf34e1164..07b6fb7cd 100644 --- a/controller/konnect/sdkfactory.go +++ b/controller/konnect/sdkfactory.go @@ -2,7 +2,7 @@ package konnect import ( sdkkonnectgo "github.com/Kong/sdk-konnect-go" - sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" ) // SDKToken is a token used to authenticate with the Konnect SDK. @@ -24,7 +24,7 @@ func NewSDKFactory() SDKFactory { func (f sdkFactory) NewKonnectSDK(serverURL string, token SDKToken) *sdkkonnectgo.SDK { return sdkkonnectgo.New( sdkkonnectgo.WithSecurity( - sdkkonnectgocomp.Security{ + sdkkonnectcomp.Security{ PersonalAccessToken: sdkkonnectgo.String(string(token)), }, ), diff --git a/controller/konnect/watch.go b/controller/konnect/watch.go index da50897b0..4b9001f70 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -6,6 +6,8 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "github.com/kong/gateway-operator/controller/konnect/constraints" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" configurationv1beta1 "github.com/kong/kubernetes-configuration/api/configuration/v1beta1" @@ -15,8 +17,8 @@ import ( // ReconciliationWatchOptionsForEntity returns the watch options for the given // Konnect entity type. func ReconciliationWatchOptionsForEntity[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ]( cl client.Client, ent TEnt, diff --git a/controller/konnect/watch_test.go b/controller/konnect/watch_test.go index df7f618ff..a3d62e59d 100644 --- a/controller/konnect/watch_test.go +++ b/controller/konnect/watch_test.go @@ -6,6 +6,8 @@ import ( "github.com/stretchr/testify/require" fakectrlruntimeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/kong/gateway-operator/controller/konnect/constraints" + configurationv1 "github.com/kong/kubernetes-configuration/api/configuration/v1" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" @@ -19,8 +21,8 @@ func TestWatchOptions(t *testing.T) { } func testReconciliationWatchOptionsForEntity[ - T SupportedKonnectEntityType, - TEnt EntityType[T], + T constraints.SupportedKonnectEntityType, + TEnt constraints.EntityType[T], ]( t *testing.T, ent TEnt, diff --git a/go.mod b/go.mod index b26476829..df5743961 100644 --- a/go.mod +++ b/go.mod @@ -87,6 +87,7 @@ require ( github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/texttheater/golang-levenshtein v1.0.1 // indirect github.com/tidwall/gjson v1.17.3 // indirect github.com/tidwall/match v1.1.1 // indirect diff --git a/test/integration/test_konnect_entities.go b/test/integration/test_konnect_entities.go index a542fd76c..6094132f7 100644 --- a/test/integration/test_konnect_entities.go +++ b/test/integration/test_konnect_entities.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectcomp "github.com/Kong/sdk-konnect-go/models/components" "github.com/google/uuid" "github.com/samber/lo" "github.com/stretchr/testify/assert" @@ -61,9 +61,9 @@ func TestKonnectEntities(t *testing.T) { Namespace: ns.Name, }, Spec: konnectv1alpha1.KonnectGatewayControlPlaneSpec{ - CreateControlPlaneRequest: components.CreateControlPlaneRequest{ + CreateControlPlaneRequest: sdkkonnectcomp.CreateControlPlaneRequest{ Name: cpName, - ClusterType: lo.ToPtr(components.ClusterTypeClusterTypeControlPlane), + ClusterType: lo.ToPtr(sdkkonnectcomp.ClusterTypeClusterTypeControlPlane), Labels: map[string]string{"test_id": testID}, }, KonnectConfiguration: konnectv1alpha1.KonnectConfiguration{