diff --git a/.golangci.yaml b/.golangci.yaml index c99fe7bc8..aab641806 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -62,6 +62,11 @@ linters-settings: - pkg: github.com/kong/gateway-operator/internal/types alias: gwtypes + + - pkg: github.com/Kong/sdk-konnect-go/models/components + alias: sdkkonnectgocomp + - pkg: github.com/Kong/sdk-konnect-go/models/operations + alias: sdkkonnectgoops revive: rules: - name: errorf diff --git a/.mockery.yaml b/.mockery.yaml new file mode 100644 index 000000000..8497c5f34 --- /dev/null +++ b/.mockery.yaml @@ -0,0 +1,14 @@ +quiet: False +inpackage: True +disable-version-string: True +with-expecter: True + +filename: "{{.InterfaceName | lower}}_mock_test.go" +dir: "{{.InterfaceDir}}" +mockname: "Mock{{.InterfaceName}}" +outpkg: "{{.PackageName}}" + +packages: + github.com/kong/gateway-operator/controller/konnect/ops: + interfaces: + ControlPlaneSDK: diff --git a/.tools_versions.yaml b/.tools_versions.yaml index 6002156fc..fb34c2271 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 5d165fd39..273443b29 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 # ------------------------------------------------------------------------------ @@ -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 82% rename from controller/konnect/constraints.go rename to controller/konnect/constraints/constraints.go index 3d8e86be4..5c16b524b 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" @@ -10,7 +10,7 @@ import ( konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) -// SupportedKonnectEntityType is an interface that all Konnect entity types +// constraints.SupportedKonnectEntityType is an interface that all Konnect entity types // must implement. type SupportedKonnectEntityType interface { konnectv1alpha1.KonnectControlPlane | @@ -23,10 +23,10 @@ type SupportedKonnectEntityType interface { GetTypeName() string } -// EntityType is an interface that all Konnect entity types must implement. -// Separating this from SupportedKonnectEntityType allows us to use EntityType +// constraints.EntityType is an interface that all Konnect entity types must implement. +// Separating this from constraints.SupportedKonnectEntityType allows us to use constraints.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..05857da00 --- /dev/null +++ b/controller/konnect/constraints/entitytypename.go @@ -0,0 +1,6 @@ +package constraints + +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 a1d8908a4..d37019152 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 KonnectControlPlane that does not exist. type ReferencedControlPlaneDoesNotExistError struct { diff --git a/controller/konnect/ops/controlplane.go b/controller/konnect/ops/controlplane.go new file mode 100644 index 000000000..2b58dfe50 --- /dev/null +++ b/controller/konnect/ops/controlplane.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectgoops "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 sdkkonnectgocomp.CreateControlPlaneRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateControlPlaneResponse, error) + DeleteControlPlane(ctx context.Context, id string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteControlPlaneResponse, error) + UpdateControlPlane(ctx context.Context, id string, req sdkkonnectgocomp.UpdateControlPlaneRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpdateControlPlaneResponse, error) +} diff --git a/controller/konnect/ops/controlplanesdk_mock_test.go b/controller/konnect/ops/controlplanesdk_mock_test.go new file mode 100644 index 000000000..006ab5a77 --- /dev/null +++ b/controller/konnect/ops/controlplanesdk_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..ba705bab4 --- /dev/null +++ b/controller/konnect/ops/kongconsumer.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectgoops "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 sdkkonnectgocomp.ConsumerInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateConsumerResponse, error) + UpsertConsumer(ctx context.Context, upsertConsumerRequest sdkkonnectgoops.UpsertConsumerRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertConsumerResponse, error) + DeleteConsumer(ctx context.Context, controlPlaneID string, consumerID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteConsumerResponse, error) +} diff --git a/controller/konnect/ops/kongroute.go b/controller/konnect/ops/kongroute.go new file mode 100644 index 000000000..0473cee1a --- /dev/null +++ b/controller/konnect/ops/kongroute.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectgoops "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 sdkkonnectgocomp.RouteInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateRouteResponse, error) + UpsertRoute(ctx context.Context, req sdkkonnectgoops.UpsertRouteRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertRouteResponse, error) + DeleteRoute(ctx context.Context, controlPlaneID, routeID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteRouteResponse, error) +} diff --git a/controller/konnect/ops/kongservice.go b/controller/konnect/ops/kongservice.go new file mode 100644 index 000000000..137082b9a --- /dev/null +++ b/controller/konnect/ops/kongservice.go @@ -0,0 +1,15 @@ +package ops + +import ( + "context" + + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectgoops "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 sdkkonnectgocomp.ServiceInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateServiceResponse, error) + UpsertService(ctx context.Context, req sdkkonnectgoops.UpsertServiceRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertServiceResponse, error) + DeleteService(ctx context.Context, controlPlaneID, serviceID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteServiceResponse, error) +} diff --git a/controller/konnect/ops.go b/controller/konnect/ops/ops.go similarity index 88% rename from controller/konnect/ops.go rename to controller/konnect/ops/ops.go index 5f1073841..052ee9269 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" @@ -39,8 +41,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, cl client.Client, e *T) (*T, error) { defer logOpComplete[T, TEnt](ctx, time.Now(), CreateOp, e) @@ -65,8 +67,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() == "" { @@ -99,12 +101,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) ) @@ -112,7 +114,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 @@ -156,8 +158,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 { @@ -168,7 +170,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(), ) } @@ -176,8 +178,8 @@ 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 { if e == nil { diff --git a/controller/konnect/ops_controlplane.go b/controller/konnect/ops/ops_controlplane.go similarity index 81% rename from controller/konnect/ops_controlplane.go rename to controller/konnect/ops/ops_controlplane.go index 5f951017d..a0eb54d07 100644 --- a/controller/konnect/ops_controlplane.go +++ b/controller/konnect/ops/ops_controlplane.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -6,23 +6,16 @@ import ( sdkkonnectgo "github.com/Kong/sdk-konnect-go" "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" 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" ) -// ControlPlaneSDK is the interface for the Konnect ControlPlane SDK. -type ControlPlaneSDK interface { - CreateControlPlane(ctx context.Context, req components.CreateControlPlaneRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateControlPlaneResponse, error) - DeleteControlPlane(ctx context.Context, id string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteControlPlaneResponse, error) - UpdateControlPlane(ctx context.Context, id string, req components.UpdateControlPlaneRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpdateControlPlaneResponse, error) -} - func createControlPlane( ctx context.Context, sdk ControlPlaneSDK, @@ -36,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(), @@ -50,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(), ), @@ -134,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(), @@ -151,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..49938111d --- /dev/null +++ b/controller/konnect/ops/ops_controlplane_test.go @@ -0,0 +1,112 @@ +package ops + +import ( + "context" + "testing" + + components "github.com/Kong/sdk-konnect-go/models/components" + operations "github.com/Kong/sdk-konnect-go/models/operations" + "github.com/stretchr/testify/assert" + + 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.KonnectControlPlane) + expectedErr error + assertions func(*testing.T, *konnectv1alpha1.KonnectControlPlane) + }{ + { + name: "Successful creation", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectControlPlane{ + Spec: konnectv1alpha1.KonnectControlPlaneSpec{ + CreateControlPlaneRequest: components.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + } + sdk.On("CreateControlPlane", ctx, cp.Spec.CreateControlPlaneRequest). + Return(&operations.CreateControlPlaneResponse{ + ControlPlane: &components.ControlPlane{ + ID: "12345", + }, + }, nil) + + return sdk, cp + }, + assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectControlPlane) { + assert.Equal(t, "12345", cp.Status.GetKonnectID()) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, cp := tc.mockCPPair() + + err := createControlPlane(ctx, sdk, cp) + if tc.expectedErr != nil { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} + +func TestDeleteControlPlane(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockCPPair func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectControlPlane) + expectedErr error + assertions func(*testing.T, *konnectv1alpha1.KonnectControlPlane) + }{ + { + name: "Successful deletion", + mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectControlPlane) { + sdk := &MockControlPlaneSDK{} + cp := &konnectv1alpha1.KonnectControlPlane{ + Spec: konnectv1alpha1.KonnectControlPlaneSpec{ + CreateControlPlaneRequest: components.CreateControlPlaneRequest{ + Name: "cp-1", + }, + }, + Status: konnectv1alpha1.KonnectControlPlaneStatus{ + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "12345", + }, + }, + } + sdk.On("DeleteControlPlane", ctx, "12345"). + Return(&operations.DeleteControlPlaneResponse{ + StatusCode: 200, + }, nil) + + 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.expectedErr != nil { + 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 85% rename from controller/konnect/ops_kongconsumer.go rename to controller/konnect/ops/ops_kongconsumer.go index d92429ed9..d5f6aad96 100644 --- a/controller/konnect/ops_kongconsumer.go +++ b/controller/konnect/ops/ops_kongconsumer.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -13,6 +13,7 @@ import ( "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" @@ -20,13 +21,6 @@ import ( "github.com/kong/kubernetes-configuration/pkg/metadata" ) -// ConsumersSDK is the interface for the Konnect Consumers SDK. -type ConsumersSDK interface { - CreateConsumer(ctx context.Context, controlPlaneID string, consumerInput sdkkonnectgocomp.ConsumerInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateConsumerResponse, error) - UpsertConsumer(ctx context.Context, upsertConsumerRequest sdkkonnectgoops.UpsertConsumerRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertConsumerResponse, error) - DeleteConsumer(ctx context.Context, controlPlaneID string, consumerID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteConsumerResponse, error) -} - func createConsumer( ctx context.Context, sdk ConsumersSDK, @@ -47,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(), @@ -61,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(), ), @@ -120,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(), @@ -135,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(), ), diff --git a/controller/konnect/ops_kongroute.go b/controller/konnect/ops/ops_kongroute.go similarity index 89% rename from controller/konnect/ops_kongroute.go rename to controller/konnect/ops/ops_kongroute.go index a830e36d2..e11f8dcef 100644 --- a/controller/konnect/ops_kongroute.go +++ b/controller/konnect/ops/ops_kongroute.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -14,19 +14,13 @@ import ( "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" ) -// RoutesSDK is the interface for the Konnect Routes SDK. -type RoutesSDK interface { - CreateRoute(ctx context.Context, controlPlaneID string, route sdkkonnectgocomp.RouteInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateRouteResponse, error) - UpsertRoute(ctx context.Context, req sdkkonnectgoops.UpsertRouteRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertRouteResponse, error) - DeleteRoute(ctx context.Context, controlPlaneID, routeID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteRouteResponse, error) -} - func createRoute( ctx context.Context, sdk RoutesSDK, @@ -44,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(), @@ -58,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(), ), @@ -136,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(), @@ -151,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(), ), diff --git a/controller/konnect/ops_kongservice.go b/controller/konnect/ops/ops_kongservice.go similarity index 87% rename from controller/konnect/ops_kongservice.go rename to controller/konnect/ops/ops_kongservice.go index f42aa60b2..c5ad6ea24 100644 --- a/controller/konnect/ops_kongservice.go +++ b/controller/konnect/ops/ops_kongservice.go @@ -1,4 +1,4 @@ -package konnect +package ops import ( "context" @@ -13,19 +13,13 @@ import ( "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" ) -// ServicesSDK is the interface for the Konnect Service SDK. -type ServicesSDK interface { - CreateService(ctx context.Context, controlPlaneID string, service sdkkonnectgocomp.ServiceInput, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.CreateServiceResponse, error) - UpsertService(ctx context.Context, req sdkkonnectgoops.UpsertServiceRequest, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.UpsertServiceResponse, error) - DeleteService(ctx context.Context, controlPlaneID, serviceID string, opts ...sdkkonnectgoops.Option) (*sdkkonnectgoops.DeleteServiceResponse, error) -} - func createService( ctx context.Context, sdk ServicesSDK, @@ -46,7 +40,7 @@ func createService( if errWrapped := wrapErrIfKonnectOpFailed(err, CreateOp, svc); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -60,9 +54,9 @@ func createService( svc.Status.Konnect.SetKonnectID(*resp.Service.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", svc.GetGeneration(), ), @@ -120,7 +114,7 @@ func updateService( if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, svc); errWrapped != nil { k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, "FailedToCreate", errWrapped.Error(), @@ -135,9 +129,9 @@ func updateService( svc.Status.Konnect.SetControlPlaneID(cp.Status.ID) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( - KonnectEntityProgrammedConditionType, + conditions.KonnectEntityProgrammedConditionType, metav1.ConditionTrue, - KonnectEntityProgrammedReasonProgrammed, + conditions.KonnectEntityProgrammedReasonProgrammed, "", svc.GetGeneration(), ), diff --git a/controller/konnect/reconciler_generic.go b/controller/konnect/reconciler_generic.go index 0489d2266..66fac08f5 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" @@ -34,7 +37,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 @@ -43,12 +46,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]) { @@ -59,8 +62,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, @@ -91,7 +94,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, }) @@ -108,7 +111,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) ) @@ -180,9 +183,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 @@ -193,9 +196,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 @@ -205,15 +208,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 @@ -222,17 +225,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 @@ -244,17 +247,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 @@ -266,9 +269,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 @@ -298,12 +301,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 @@ -328,7 +331,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 @@ -340,8 +343,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, } } @@ -368,7 +372,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 @@ -426,7 +430,7 @@ func updateStatusWithCondition[T interface { } return ctrl.Result{}, fmt.Errorf( "failed to update status with %s condition: %w", - KonnectEntityAPIAuthConfigurationResolvedRefConditionType, err, + conditions.KonnectEntityAPIAuthConfigurationResolvedRefConditionType, err, ) } @@ -473,7 +477,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, @@ -510,7 +514,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 @@ -524,7 +528,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) { @@ -545,7 +549,7 @@ func getServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( // handlKongServiceRef 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 handlKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( +func handlKongServiceRef[T constraints.SupportedKonnectEntityType, TEnt constraints.EntityType[T]]( ctx context.Context, cl client.Client, ent TEnt, @@ -566,9 +570,9 @@ func handlKongServiceRef[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 @@ -585,14 +589,14 @@ func handlKongServiceRef[T SupportedKonnectEntityType, TEnt EntityType[T]]( } } - cond, ok := k8sutils.GetCondition(KonnectEntityProgrammedConditionType, &svc) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, &svc) if !ok || cond.Status != metav1.ConditionTrue /*|| cond.ObservedGeneration != cp.GetGeneration() */ { 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 @@ -623,9 +627,9 @@ func handlKongServiceRef[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 @@ -642,9 +646,9 @@ func handlKongServiceRef[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 @@ -658,13 +662,13 @@ func handlKongServiceRef[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 @@ -682,9 +686,9 @@ func handlKongServiceRef[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 @@ -697,7 +701,7 @@ func handlKongServiceRef[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) { @@ -721,7 +725,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, @@ -742,9 +746,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 @@ -758,13 +762,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 @@ -793,9 +797,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 2df8a82de..f5f1dc98d 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" @@ -26,8 +27,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 3125e5cce..82c97c9f2 100644 --- a/controller/konnect/reconciler_konnectapiauth.go +++ b/controller/konnect/reconciler_konnectapiauth.go @@ -16,6 +16,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" @@ -119,9 +120,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 @@ -146,9 +147,9 @@ func (r *KonnectAPIAuthConfigurationReconciler) Reconcile( respOrg, err := sdk.Me.GetOrganizationsMe(ctx, sdkkonnectgoops.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 { @@ -158,9 +159,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 { @@ -183,10 +184,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 { @@ -196,9 +197,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/watch.go b/controller/konnect/watch.go index 87b6ce17e..752ed0558 100644 --- a/controller/konnect/watch.go +++ b/controller/konnect/watch.go @@ -9,13 +9,15 @@ import ( 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" + + "github.com/kong/gateway-operator/controller/konnect/constraints" ) // 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 0a406a2eb..c80e96a20 100644 --- a/controller/konnect/watch_test.go +++ b/controller/konnect/watch_test.go @@ -9,6 +9,8 @@ import ( 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" + + "github.com/kong/gateway-operator/controller/konnect/constraints" ) func TestWatchOptions(t *testing.T) { @@ -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 a9536885f..eaa249791 100644 --- a/go.mod +++ b/go.mod @@ -86,6 +86,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