diff --git a/.golangci.yaml b/.golangci.yaml index b104749451..2b2edf15a2 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -47,7 +47,7 @@ linters: alias: declarativetest - pkg: github.com/kyma-project/lifecycle-manager/tests/integration/controller/manifest alias: manifesttest - - pkg: github.com/kyma-project/runtime-watcher/listener/pkg/event + - pkg: github.com/kyma-project/runtime-watcher/listener/pkg/v2/event alias: watcherevent - pkg: github.com/kyma-project/runtime-watcher/listener/pkg/metrics alias: watchermetrics diff --git a/Makefile b/Makefile index 012b0a5543..4187992f7f 100644 --- a/Makefile +++ b/Makefile @@ -76,13 +76,6 @@ test: unittest manifests test-crd generate fmt vet envtest ## Run tests. unittest: ## Run the unit test suite. $(GO) test `go list ./... | grep -v /tests/` -coverprofile cover.out -coverpkg=./... -.PHONY: dry-run -dry-run: kustomize manifests - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - mkdir -p dry-run - $(KUSTOMIZE) build config/default > dry-run/manifests.yaml - - .PHONY: dry-run-control-plane dry-run-control-plane: kustomize manifests cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} @@ -124,11 +117,6 @@ install: manifests kustomize ## Install CRDs into the K8s cluster specified in ~ uninstall: manifests kustomize ## Uninstall CRDs from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. $(KUSTOMIZE) build config/crd | kubectl delete --ignore-not-found=$(ignore-not-found) -f - -.PHONY: deploy -deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in ~/.kube/config. - cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} - $(KUSTOMIZE) build config/default | kubectl apply -f - - .PHONY: local-deploy-with-watcher local-deploy-with-watcher: generate kustomize ## Deploy the controller locally with the watcher component using cert-manager for certificate management. cd config/manager && $(KUSTOMIZE) edit set image controller=${IMG} @@ -141,7 +129,7 @@ local-deploy-with-watcher-gcm: generate kustomize ## Deploy the controller local .PHONY: undeploy undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/config. Call with ignore-not-found=true to ignore resource not found errors during deletion. - $(KUSTOMIZE) build config/default | kubectl delete --ignore-not-found=$(ignore-not-found) -f - + $(KUSTOMIZE) build config/control-plane | kubectl delete --ignore-not-found=$(ignore-not-found) -f - ## Location to install dependencies to LOCALBIN ?= $(shell pwd)/bin diff --git a/config/watcher/kustomization.yaml b/config/watcher/kustomization.yaml index ce65e9e576..8702526ca8 100644 --- a/config/watcher/kustomization.yaml +++ b/config/watcher/kustomization.yaml @@ -17,7 +17,7 @@ patches: value: --skr-watcher-path=/skr-webhook - op: add path: /spec/template/spec/containers/0/args/- - value: --skr-watcher-image-tag=1.2.0 + value: --skr-watcher-image-tag=2.0.0 - op: add path: /spec/template/spec/containers/0/args/- value: --skr-watcher-image-registry=europe-docker.pkg.dev/kyma-project/prod diff --git a/go.mod b/go.mod index 202e1798ba..8b99142f1e 100644 --- a/go.mod +++ b/go.mod @@ -19,11 +19,11 @@ require ( github.com/jellydator/ttlcache/v3 v3.4.0 github.com/kyma-project/lifecycle-manager/api v0.0.0-00010101000000-000000000000 github.com/kyma-project/lifecycle-manager/maintenancewindows v0.0.0-20250113095044-41115399d588 - github.com/kyma-project/runtime-watcher/listener v0.0.0-20240502124257-9d96561ef070 + github.com/kyma-project/runtime-watcher/listener v0.0.0-20250825111833-2cf3f8cc5232 github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/gomega v1.38.0 github.com/prometheus/client_golang v1.23.0 - github.com/stretchr/testify v1.10.0 + github.com/stretchr/testify v1.11.0 go.uber.org/zap v1.27.0 golang.org/x/sync v0.16.0 golang.org/x/time v0.12.0 @@ -42,11 +42,11 @@ require ( github.com/go-co-op/gocron v1.37.0 github.com/kyma-project/template-operator/api v0.0.0-20240404131948-52c84f14e73c github.com/prometheus/client_model v0.6.2 - k8s.io/api v0.33.3 + k8s.io/api v0.33.4 k8s.io/apiextensions-apiserver v0.33.3 - k8s.io/apimachinery v0.33.3 + k8s.io/apimachinery v0.33.4 k8s.io/cli-runtime v0.33.3 - k8s.io/client-go v0.33.3 + k8s.io/client-go v0.33.4 k8s.io/kubectl v0.33.3 ) diff --git a/go.sum b/go.sum index bce07c9c9b..28fff4a987 100644 --- a/go.sum +++ b/go.sum @@ -606,8 +606,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/kyma-project/runtime-watcher/listener v0.0.0-20240502124257-9d96561ef070 h1:AFegQ/P12b1jTZ90ydCQd7h/pVaj1XZkcYXwLHZYSPk= -github.com/kyma-project/runtime-watcher/listener v0.0.0-20240502124257-9d96561ef070/go.mod h1:MbLimL7PbR8lDadZ0Po9irrpAf1S2v6YymFFST/HYFA= +github.com/kyma-project/runtime-watcher/listener v0.0.0-20250822075504-353a2aaa9894 h1:9coOPviV16M+EZKrOfhsMrmScX+bH93C4UAlkl8EL6k= +github.com/kyma-project/runtime-watcher/listener v0.0.0-20250822075504-353a2aaa9894/go.mod h1:dmTTjYNhk6o/Frb2xRzk7R2nZxn1Rw1dEblM/auQMHs= +github.com/kyma-project/runtime-watcher/listener v0.0.0-20250825111833-2cf3f8cc5232 h1:4jPg3Ey0ie5ipYCR72VuxsCR3139uywIIYIfjZs+zQs= +github.com/kyma-project/runtime-watcher/listener v0.0.0-20250825111833-2cf3f8cc5232/go.mod h1:KdQvykzVhghVhwLZJbrXUoV1YplGVnxe2wgAfMDWoXM= +github.com/kyma-project/runtime-watcher/listener v1.1.18 h1:vJJJnhagHyhnxnoZhVvOAxU8SH6Ex3X6TXc9J79hbDg= +github.com/kyma-project/runtime-watcher/listener v1.1.18/go.mod h1:c9h0QMzzlc7DSP09OI0c+vwV8iR0jdp623a9E9+FivY= github.com/kyma-project/template-operator/api v0.0.0-20240404131948-52c84f14e73c h1:alsRB1f5TaNNOCg8f1DEI/74lk5lpq6bbNmi8PZpFxI= github.com/kyma-project/template-operator/api v0.0.0-20240404131948-52c84f14e73c/go.mod h1:WpDVtbu62bjMTNY2kWThlw6iffWRrG3zERdRwZHnwv8= github.com/letsencrypt/boulder v0.0.0-20241010192615-6692160cedfa h1:/kPrcWfMENmWJh9AceGWaTJ0QBS3OyCDENx2vI71T8k= @@ -636,6 +640,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/medmes/runtime-watcher/listener v0.0.0-20250810204720-3bfb15765847 h1:U6iMAEONChS8sSjzb4EmpSC+RPAozfu1mmqFMFVbKLk= +github.com/medmes/runtime-watcher/listener v0.0.0-20250810204720-3bfb15765847/go.mod h1:gqXEeFh/TLX8yc16yWdn0DDqriRzI+yHrQS8xyqAenQ= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/miekg/pkcs11 v1.0.3-0.20190429190417-a667d056470f/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= @@ -855,6 +861,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.0 h1:ib4sjIrwZKxE5u/Japgo/7SJV3PvgjGiRNAvTVGqQl8= +github.com/stretchr/testify v1.11.0/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= @@ -1216,14 +1224,20 @@ istio.io/client-go v1.26.3 h1:ryF4+Nyz5wDO4mVCzXcm2W+fqbnekY88Z36hTcv5fnw= istio.io/client-go v1.26.3/go.mod h1:u2p5L7UvjNswrrlHZ+QMlUOjERK2sXputywzyNhtTMg= k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/api v0.33.4 h1:oTzrFVNPXBjMu0IlpA2eDDIU49jsuEorGHB4cvKupkk= +k8s.io/api v0.33.4/go.mod h1:VHQZ4cuxQ9sCUMESJV5+Fe8bGnqAARZ08tSTdHWfeAc= k8s.io/apiextensions-apiserver v0.33.3 h1:qmOcAHN6DjfD0v9kxL5udB27SRP6SG/MTopmge3MwEs= k8s.io/apiextensions-apiserver v0.33.3/go.mod h1:oROuctgo27mUsyp9+Obahos6CWcMISSAPzQ77CAQGz8= k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apimachinery v0.33.4 h1:SOf/JW33TP0eppJMkIgQ+L6atlDiP/090oaX0y9pd9s= +k8s.io/apimachinery v0.33.4/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/cli-runtime v0.33.3 h1:Dgy4vPjNIu8LMJBSvs8W0LcdV0PX/8aGG1DA1W8lklA= k8s.io/cli-runtime v0.33.3/go.mod h1:yklhLklD4vLS8HNGgC9wGiuHWze4g7x6XQZ+8edsKEo= k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/client-go v0.33.4 h1:TNH+CSu8EmXfitntjUPwaKVPN0AYMbc9F1bBS8/ABpw= +k8s.io/client-go v0.33.4/go.mod h1:LsA0+hBG2DPwovjd931L/AoaezMPX9CmBgyVyBZmbCY= k8s.io/component-base v0.33.3 h1:mlAuyJqyPlKZM7FyaoM/LcunZaaY353RXiOd2+B5tGA= k8s.io/component-base v0.33.3/go.mod h1:ktBVsBzkI3imDuxYXmVxZ2zxJnYTZ4HAsVj9iF09qp4= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= diff --git a/internal/common/interfaces.go b/internal/common/interfaces.go new file mode 100644 index 0000000000..7e9a5674f0 --- /dev/null +++ b/internal/common/interfaces.go @@ -0,0 +1,21 @@ +package common + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// EventService defines the interface for event services consumed by controllers. +// Moved to common package to avoid circular dependencies between service and controller packages. +type EventService interface { + // Start begins the event service lifecycle + Start(ctx context.Context) error + + // Stop shuts down the event service + Stop() error + + // CreateEventSource creates a controller-runtime compatible event source + CreateEventSource(eventHandler handler.EventHandler) source.Source +} diff --git a/internal/controller/interfaces.go b/internal/controller/interfaces.go new file mode 100644 index 0000000000..d3c780f434 --- /dev/null +++ b/internal/controller/interfaces.go @@ -0,0 +1,9 @@ +package controller + +import ( + "github.com/kyma-project/lifecycle-manager/internal/common" +) + +// EventService re-exports the common EventService interface for backward compatibility. +// This allows controllers to use controller.EventService while avoiding circular dependencies. +type EventService = common.EventService diff --git a/internal/controller/kyma/setup.go b/internal/controller/kyma/setup.go index 407bebf0cc..e9672be263 100644 --- a/internal/controller/kyma/setup.go +++ b/internal/controller/kyma/setup.go @@ -4,26 +4,20 @@ import ( "context" "errors" "fmt" - "net/http" - watcherevent "github.com/kyma-project/runtime-watcher/listener/pkg/event" - "github.com/kyma-project/runtime-watcher/listener/pkg/types" apicorev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntime "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/service/skrevent" "github.com/kyma-project/lifecycle-manager/internal/watch" - "github.com/kyma-project/lifecycle-manager/pkg/security" ) type SetupOptions struct { @@ -34,29 +28,24 @@ type SetupOptions struct { const controllerName = "kyma" -var ( - errConvertingWatched = errors.New("error converting watched to object key") - errParsingWatched = errors.New("error getting watched object from unstructured event") - errConvertingWatcherEvent = errors.New("error converting watched object to unstructured event") -) +var errConvertingWatcherEvent = errors.New("error converting watched object to unstructured event") func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options, settings SetupOptions) error { - var verifyFunc watcherevent.Verify - if settings.EnableDomainNameVerification { - verifyFunc = security.NewRequestVerifier(mgr.GetClient()).Verify - } else { - verifyFunc = func(r *http.Request, watcherEvtObject *types.WatchEvent) error { - return nil - } - } - runnableListener := watcherevent.NewSKREventListener( + runtimeEventService, err := skrevent.NewSKREventService( + mgr, settings.ListenerAddr, - shared.OperatorName, - verifyFunc, + "kyma", // Component name for Kyma controller events + settings.EnableDomainNameVerification, ) - if err := mgr.Add(runnableListener); err != nil { - return fmt.Errorf("KymaReconciler %w", err) + if err != nil { + return fmt.Errorf("failed to create runtime event service: %w", err) } + + // Note: Service is automatically started by the manager (implements manager.Runnable) + + // Create event source for this controller using the service's channel + runtimeEventSource := runtimeEventService.CreateEventSource(r.runtimeEventHandler()) + if err := ctrl.NewControllerManagedBy(mgr).For(&v1beta2.Kyma{}). Named(controllerName). WithOptions(opts). @@ -68,7 +57,7 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options Watches(&v1beta2.Manifest{}, handler.EnqueueRequestForOwner(mgr.GetScheme(), mgr.GetRESTMapper(), &v1beta2.Kyma{}, handler.OnlyControllerOwner()), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). - WatchesRawSource(source.Channel(runnableListener.ReceivedEvents, r.skrEventHandler())). + WatchesRawSource(runtimeEventSource). Complete(r); err != nil { return fmt.Errorf("failed to setup manager for kyma controller: %w", err) } @@ -76,36 +65,31 @@ func (r *Reconciler) SetupWithManager(mgr ctrl.Manager, opts ctrlruntime.Options return nil } -func (r *Reconciler) skrEventHandler() *handler.Funcs { +func (r *Reconciler) runtimeEventHandler() *handler.Funcs { return &handler.Funcs{ GenericFunc: func(ctx context.Context, evnt event.GenericEvent, queue workqueue.TypedRateLimitingInterface[ctrl.Request], ) { - logger := ctrl.Log.WithName("listener") + logger := ctrl.Log.WithName("kyma-listener") unstructWatcherEvt, conversionOk := evnt.Object.(*unstructured.Unstructured) if !conversionOk { logger.Error(errConvertingWatcherEvent, fmt.Sprintf("event: %v", evnt.Object)) return } - // get owner object from unstructured event, owner = KymaCR object reference in KCP - unstructuredOwner, ok := unstructWatcherEvt.Object["owner"] - if !ok { - logger.Error(errParsingWatched, fmt.Sprintf("unstructured event: %v", unstructWatcherEvt)) - return - } - - ownerObjectKey, conversionOk := unstructuredOwner.(client.ObjectKey) - if !conversionOk { - logger.Error(errConvertingWatched, fmt.Sprintf("unstructured Owner object: %v", unstructuredOwner)) + // This is where ExtractOwnerKey is essential - it tells us WHICH Kyma to reconcile + ownerObjectKey, err := skrevent.ExtractOwnerKey(unstructWatcherEvt) + if err != nil { + logger.Error(err, "failed to extract owner key from runtime watcher event") return } logger.Info( - fmt.Sprintf("event received from SKR, adding %s to queue", + fmt.Sprintf("kyma event received from runtime-watcher, adding %s to queue", ownerObjectKey), ) + // The extracted owner key becomes the reconciliation target queue.Add(ctrl.Request{ NamespacedName: ownerObjectKey, }) diff --git a/internal/controller/manifest/setup.go b/internal/controller/manifest/setup.go index 1272acb9c5..3d321296e2 100644 --- a/internal/controller/manifest/setup.go +++ b/internal/controller/manifest/setup.go @@ -3,33 +3,32 @@ package manifest import ( "context" "fmt" - "net/http" - "strings" - watcherevent "github.com/kyma-project/runtime-watcher/listener/pkg/event" - "github.com/kyma-project/runtime-watcher/listener/pkg/types" apicorev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" ctrlruntime "sigs.k8s.io/controller-runtime/pkg/controller" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/manager" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/source" - "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/common" declarativev2 "github.com/kyma-project/lifecycle-manager/internal/declarative/v2" "github.com/kyma-project/lifecycle-manager/internal/manifest/spec" "github.com/kyma-project/lifecycle-manager/internal/pkg/metrics" "github.com/kyma-project/lifecycle-manager/internal/service/manifest/orphan" + "github.com/kyma-project/lifecycle-manager/internal/service/skrevent" "github.com/kyma-project/lifecycle-manager/pkg/queue" - "github.com/kyma-project/lifecycle-manager/pkg/security" ) +// EventService defines the interface for event services consumed by this controller. +// Points to the shared common interface to avoid circular dependencies. +type EventService = common.EventService + const controllerName = "manifest" type SetupOptions struct { @@ -42,48 +41,56 @@ func SetupWithManager(mgr manager.Manager, opts ctrlruntime.Options, requeueInte mandatoryModulesMetrics *metrics.MandatoryModulesMetrics, manifestClient declarativev2.ManifestAPIClient, orphanDetectionClient orphan.DetectionRepository, specResolver *spec.Resolver, ) error { - var verifyFunc watcherevent.Verify - if settings.EnableDomainNameVerification { - // Verifier used to verify incoming listener requests - verifyFunc = security.NewRequestVerifier(mgr.GetClient()).Verify - } else { - verifyFunc = func(r *http.Request, watcherEvtObject *types.WatchEvent) error { - return nil - } + // Create runtime event service directly - simple and clean + runtimeEventService, err := skrevent.NewSKREventService( + mgr, + settings.ListenerAddr, + "manifest", // Component name for Manifest controller events + settings.EnableDomainNameVerification, + ) + if err != nil { + return fmt.Errorf("failed to create runtime event service for manifest controller: %w", err) } - runnableListener := watcherevent.NewSKREventListener( - settings.ListenerAddr, strings.ToLower(shared.OperatorName), - verifyFunc, - ) + // Use interface for clean dependency management + var eventService EventService = runtimeEventService - // start listener as a manager runnable - if err := mgr.Add(runnableListener); err != nil { - return fmt.Errorf("failed to add to listener to manager: %w", err) - } + // Note: Service is automatically started by the manager (implements manager.Runnable) - addSkrEventToQueueFunc := &handler.Funcs{ + // Create event handler for manifest events + addRuntimeEventToQueueFunc := &handler.Funcs{ GenericFunc: func(ctx context.Context, evnt event.GenericEvent, queue workqueue.TypedRateLimitingInterface[ctrl.Request], ) { - ctrl.Log.WithName("listener").Info( - fmt.Sprintf( - "event coming from SKR, adding %s to queue", - client.ObjectKeyFromObject(evnt.Object).String(), - ), - ) - queue.Add(ctrl.Request{NamespacedName: client.ObjectKeyFromObject(evnt.Object)}) + logger := ctrl.Log.WithName("manifest-listener") + unstructWatcherEvt, conversionOk := evnt.Object.(*unstructured.Unstructured) + if !conversionOk { + logger.Error(nil, "error converting event object", "event", evnt.Object) + return + } + + // Again, ExtractOwnerKey tells us WHICH Manifest to reconcile + ownerObjectKey, err := skrevent.ExtractOwnerKey(unstructWatcherEvt) + if err != nil { + logger.Error(err, "failed to extract owner key from runtime watcher event") + return + } + + logger.Info("manifest event from runtime-watcher, adding to queue", "objectKey", ownerObjectKey.String()) + queue.Add(ctrl.Request{NamespacedName: ownerObjectKey}) }, } - skrEventChannel := source.Channel(runnableListener.ReceivedEvents, addSkrEventToQueueFunc) + // Create event source for this controller + runtimeEventSource := eventService.CreateEventSource(addRuntimeEventToQueueFunc) + if err := ctrl.NewControllerManagedBy(mgr). For(&v1beta2.Manifest{}). Named(controllerName). Watches(&apicorev1.Secret{}, handler.Funcs{}, builder.WithPredicates(predicate.Or(predicate.GenerationChangedPredicate{}, predicate.LabelChangedPredicate{}))). - WatchesRawSource(skrEventChannel). + WatchesRawSource(runtimeEventSource). WithOptions(opts). Complete(NewReconciler(mgr, requeueIntervals, manifestMetrics, mandatoryModulesMetrics, manifestClient, orphanDetectionClient, specResolver)); err != nil { diff --git a/internal/service/skrevent/skr_event_service.go b/internal/service/skrevent/skr_event_service.go new file mode 100644 index 0000000000..6e3c0ff5bd --- /dev/null +++ b/internal/service/skrevent/skr_event_service.go @@ -0,0 +1,128 @@ +package skrevent + +import ( + "context" + "fmt" + "sync" + + watcherevent "github.com/kyma-project/runtime-watcher/listener/pkg/v2/event" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/kyma-project/lifecycle-manager/internal/common" +) + +// Implement manager.Runnable interface for automatic lifecycle management. +var _ manager.Runnable = (*SkrRuntimeEventService)(nil) + +// Implement common.EventService interface for dependency injection. +var _ common.EventService = (*SkrRuntimeEventService)(nil) + +type SkrRuntimeEventService struct { + listener *watcherevent.SKREventListener + eventChannel chan event.GenericEvent // Single channel - no pub/sub + cancel context.CancelFunc // Only store cancel func, not context + once sync.Once // Ensure channel is closed only once + cancelMutex sync.Mutex // Protect cancel function access +} + +const ( + // Default buffer size for the event channel. + defaultEventChannelBuffer = 100 +) + +// NewSkrRuntimeEventService creates a simplified event service. +func NewSkrRuntimeEventService(listener *watcherevent.SKREventListener) *SkrRuntimeEventService { + return &SkrRuntimeEventService{ + listener: listener, + eventChannel: make(chan event.GenericEvent, defaultEventChannelBuffer), + } +} + +// CreateEventSource returns the single event source. +// +//nolint:ireturn // Interface compliance required for controller-runtime integration. +func (s *SkrRuntimeEventService) CreateEventSource(handler handler.EventHandler) source.Source { + return source.Channel(s.eventChannel, handler) +} + +// Start begins simple event forwarding - implements manager.Runnable interface. +func (s *SkrRuntimeEventService) Start(ctx context.Context) error { + childCtx, cancel := context.WithCancel(ctx) + + // Thread-safe assignment of cancel function + s.cancelMutex.Lock() + s.cancel = cancel + s.cancelMutex.Unlock() + + // Start event forwarding FIRST (before listener blocks) + go s.forwardEvents(childCtx) + + // Start the listener with the context (if listener exists) + // This will block until context is cancelled + if s.listener != nil { + err := s.listener.Start(childCtx) + if err != nil { + return fmt.Errorf("failed to start SKR event listener: %w", err) + } + } + + return nil +} + +// Stop shuts down - managed automatically by controller-runtime manager +// Implements manager.Runnable interface. +func (s *SkrRuntimeEventService) Stop() error { + // Thread-safe access to cancel function + s.cancelMutex.Lock() + defer s.cancelMutex.Unlock() + + if s.cancel != nil { + s.cancel() + } + + return nil +} + +// forwardEvents - Direct 1:1 forwarding from listener to controller-runtime. +// This goroutine reads events from the listener's ReceivedEvents channel and forwards +// them to the eventChannel that controller-runtime uses as an event source. +func (s *SkrRuntimeEventService) forwardEvents(ctx context.Context) { + // Ensure channel is closed only once using sync.Once + defer s.once.Do(func() { + close(s.eventChannel) + }) + + // If no listener, just wait for context cancellation + if s.listener == nil { + <-ctx.Done() + return + } + + for { + select { + case listenerEvent, ok := <-s.listener.ReceivedEvents(): + if !ok { + return // Listener closed + } + + // Convert from listener types.GenericEvent to controller-runtime event.GenericEvent + crEvent := event.GenericEvent{ + Object: listenerEvent.Object, + } + + // Forward to single channel (no distribution logic) + select { + case s.eventChannel <- crEvent: + // Forwarded successfully + case <-ctx.Done(): + return + } + + case <-ctx.Done(): + return + } + } +} diff --git a/internal/service/skrevent/skr_event_service_test.go b/internal/service/skrevent/skr_event_service_test.go new file mode 100644 index 0000000000..9b98f4fdb9 --- /dev/null +++ b/internal/service/skrevent/skr_event_service_test.go @@ -0,0 +1,184 @@ +package skrevent_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/kyma-project/lifecycle-manager/internal/service/skrevent" +) + +// Simple tests focusing on core functionality. +func TestNewSkrRuntimeEventService_Basic(t *testing.T) { + t.Run("creates service with nil listener", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + assert.NotNil(t, service, "NewSkrRuntimeEventService should return non-nil service") + }) +} + +func TestSkrRuntimeEventService_Stop_Basic(t *testing.T) { + t.Run("stop returns no error with nil listener", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + err := service.Stop() + assert.NoError(t, err, "Stop should never return an error") + }) +} + +func TestSkrRuntimeEventService_CreateEventSource_Basic(t *testing.T) { + t.Run("creates event source with nil handler", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + source := service.CreateEventSource(nil) + assert.NotNil(t, source, "CreateEventSource should return non-nil source") + }) +} + +func TestSkrRuntimeEventService_Start_Integration(t *testing.T) { + t.Run("start with nil listener completes", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + + ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond) + defer cancel() + + // Start should not block indefinitely with nil listener + done := make(chan error, 1) + + go func() { + done <- service.Start(ctx) + }() + + select { + case err := <-done: + require.NoError(t, err, "Start should complete without error") + case <-time.After(200 * time.Millisecond): + t.Fatal("Start did not complete in reasonable time") + } + }) + + t.Run("start respects context cancellation", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + + ctx, cancel := context.WithCancel(t.Context()) + cancel() // Cancel immediately + + // Start should return quickly when context is cancelled + done := make(chan error, 1) + + go func() { + done <- service.Start(ctx) + }() + + select { + case err := <-done: + // Should complete, potentially with context.Canceled error + assert.True(t, err == nil || errors.Is(err, context.Canceled), + "Start should complete or return context.Canceled, got: %v", err) + case <-time.After(200 * time.Millisecond): + t.Fatal("Start did not respect context cancellation") + } + }) +} + +// Test basic service properties and interface compliance. +func TestSkrRuntimeEventService_Properties(t *testing.T) { + t.Run("service implements basic interface pattern", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + require.NotNil(t, service) + + // Test that basic methods exist and don't panic + assert.NotPanics(t, func() { + _ = service.Stop() + }, "Stop method should not panic") + + assert.NotPanics(t, func() { + _ = service.CreateEventSource(nil) + }, "CreateEventSource method should not panic") + + assert.NotPanics(t, func() { + ctx, cancel := context.WithCancel(t.Context()) + cancel() // Cancel immediately + + _ = service.Start(ctx) + }, "Start method should not panic") + }) + + t.Run("service can be started and stopped multiple times", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + + // Multiple stop calls should be safe + assert.NoError(t, service.Stop()) + assert.NoError(t, service.Stop()) + + // Multiple start calls with cancelled context should be safe + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + assert.NotPanics(t, func() { + _ = service.Start(ctx) + _ = service.Start(ctx) + }) + }) +} + +// Test the constructor functions signature validation. +func TestConstructorFunctions(t *testing.T) { + t.Run("NewSKREventService function signature", func(t *testing.T) { + // Test that the function signature exists and compiles + assert.NotNil(t, skrevent.NewSKREventService, "NewSKREventService function should exist") + }) +} + +// Test service behavior with edge cases. +func TestSkrRuntimeEventService_EdgeCases(t *testing.T) { + t.Run("creating skrevent source multiple times", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + + source1 := service.CreateEventSource(nil) + source2 := service.CreateEventSource(nil) + + assert.NotNil(t, source1, "First CreateEventSource should return non-nil") + assert.NotNil(t, source2, "Second CreateEventSource should return non-nil") + // Note: We don't test equality as the service might return different instances + }) + + t.Run("service handles concurrent operations", func(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + + // Run concurrent operations that should be safe + done := make(chan bool, 3) + + go func() { + _ = service.Stop() + + done <- true + }() + + go func() { + _ = service.CreateEventSource(nil) + + done <- true + }() + + go func() { + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + _ = service.Start(ctx) + + done <- true + }() + + // Wait for all operations to complete + for range 3 { + select { + case <-done: + // OK + case <-time.After(500 * time.Millisecond): + t.Fatal("Concurrent operations did not complete in reasonable time") + } + } + }) +} diff --git a/internal/service/skrevent/skr_event_service_unit_internal_test.go: b/internal/service/skrevent/skr_event_service_unit_internal_test.go: new file mode 100644 index 0000000000..aa58149e07 --- /dev/null +++ b/internal/service/skrevent/skr_event_service_unit_internal_test.go: @@ -0,0 +1,238 @@ +package skrevent + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSkrRuntimeEventService_CancelMutexProtection(t *testing.T) { + t.Run("concurrent Start and Stop operations are thread-safe", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + var wgr sync.WaitGroup + numGoroutines := 10 + errors := make([]error, numGoroutines*2) // For both Start and Stop operations + + // Launch concurrent Start and Stop operations + for gri := range numGoroutines { + wgr.Add(2) // One for Start, one for Stop + + go func(idx int) { + defer wgr.Done() + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + defer cancel() + errors[idx*2] = service.Start(ctx) + }(gri) + + go func(idx int) { + defer wgr.Done() + errors[idx*2+1] = service.Stop() + }(gri) + } + + wgr.Wait() + + // Stop operations should never return errors + for i := 1; i < len(errors); i += 2 { + assert.NoError(t, errors[i], "Stop operation should never return error") + } + }) + + t.Run("multiple concurrent Stop calls are safe", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + var wgr sync.WaitGroup + numStops := 20 + errors := make([]error, numStops) + + // Multiple concurrent Stop calls + for i := range numStops { + wgr.Add(1) + go func(idx int) { + defer wgr.Done() + errors[idx] = service.Stop() + }(i) + } + + wgr.Wait() + + // All Stop calls should succeed + for i, err := range errors { + assert.NoError(t, err, "Stop call %d should not return error", i) + } + }) +} + +func TestSkrRuntimeEventService_ForwardEventsGoroutineOrder(t *testing.T) { + t.Run("Start method returns immediately with nil listener", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // The key test: Start should return immediately when listener is nil + // (forwardEvents runs in a goroutine and waits for context) + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond) + defer cancel() + + startTime := time.Now() + err := service.Start(ctx) + elapsed := time.Since(startTime) + + // With nil listener, Start() should return immediately (< 1ms typically) + assert.Less(t, elapsed, 10*time.Millisecond, "Start should return immediately with nil listener, took %v", elapsed) + assert.NoError(t, err, "Should not return error with nil listener") + }) + + t.Run("multiple Start calls handle goroutine creation properly", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // Test multiple quick Start calls to ensure goroutines are managed properly + for range 3 { + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + + err := service.Start(ctx) + + // Should complete without error + require.NoError(t, err, "Start should not return error") + + cancel() + } + }) +} + +func TestSkrRuntimeEventService_ChannelCloseProtection(t *testing.T) { + t.Run("channel is closed only once with sync.Once", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // Start multiple forwardEvents goroutines to test sync.Once + var wgr sync.WaitGroup + numGoroutines := 5 + + for range numGoroutines { + wgr.Add(1) + go func() { + defer wgr.Done() + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + defer cancel() + service.forwardEvents(ctx) + }() + } + + wgr.Wait() + + // Verify channel is closed + select { + case _, ok := <-service.eventChannel: + assert.False(t, ok, "Channel should be closed") + default: + // Channel might be closed but no value to read, which is also fine + } + }) +} + +func TestSkrRuntimeEventService_ContextHandling(t *testing.T) { + t.Run("context cancellation is properly propagated", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // Use a very short timeout to test cancellation + ctx, cancel := context.WithTimeout(t.Context(), 1*time.Millisecond) + defer cancel() + + startTime := time.Now() + err := service.Start(ctx) + elapsed := time.Since(startTime) + + // Should complete quickly after context cancellation + assert.Less(t, elapsed, 50*time.Millisecond, "Should respect context cancellation quickly") + + // Should return context-related error + if err != nil { + assert.ErrorIs(t, err, context.DeadlineExceeded) + } + }) + + t.Run("child context is properly created and managed", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // Cancel the parent context after a short delay + ctx, cancel := context.WithCancel(t.Context()) + + go func() { + time.Sleep(10 * time.Millisecond) + cancel() + }() + + startTime := time.Now() + err := service.Start(ctx) + elapsed := time.Since(startTime) + + // Should complete reasonably quickly after parent cancellation + assert.Less(t, elapsed, 100*time.Millisecond, "Should complete quickly after parent context cancellation") + + if err != nil { + assert.ErrorIs(t, err, context.Canceled) + } + }) +} + +func TestSkrRuntimeEventService_NilListenerHandling(t *testing.T) { + t.Run("forwardEvents handles nil listener gracefully", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + ctx, cancel := context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + + // This should not panic and should complete after context cancellation + startTime := time.Now() + service.forwardEvents(ctx) + elapsed := time.Since(startTime) + + // Should complete around the timeout duration + assert.GreaterOrEqual(t, elapsed, 8*time.Millisecond, "Should wait for context cancellation") + assert.Less(t, elapsed, 50*time.Millisecond, "Should not take too long") + }) + + t.Run("Start with nil listener returns immediately", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + ctx, cancel := context.WithTimeout(t.Context(), 15*time.Millisecond) + defer cancel() + + startTime := time.Now() + err := service.Start(ctx) + elapsed := time.Since(startTime) + + // With nil listener, Start should return immediately (forwardEvents runs in goroutine) + assert.Less(t, elapsed, 10*time.Millisecond, "Start should return immediately with nil listener, took %v", elapsed) + assert.NoError(t, err, "Should not return error with nil listener") + }) +} + +func TestSkrRuntimeEventService_Initialization(t *testing.T) { + t.Run("service is properly initialized with correct fields", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + // Verify service structure + assert.NotNil(t, service, "Service should be created") + assert.NotNil(t, service.eventChannel, "Event channel should be initialized") + assert.Equal(t, 100, cap(service.eventChannel), "Event channel should have buffer size 100") + + // Initial state checks + assert.Nil(t, service.listener, "Listener should be nil as provided") + assert.Nil(t, service.cancel, "Cancel function should be nil initially") + }) + + t.Run("CreateEventSource returns valid source", func(t *testing.T) { + service := NewSkrRuntimeEventService(nil) + + source := service.CreateEventSource(nil) + assert.NotNil(t, source, "Event source should be created") + + // Multiple calls should work + source2 := service.CreateEventSource(nil) + assert.NotNil(t, source2, "Second event source should also be created") + }) +} diff --git a/internal/service/skrevent/skr_events.go b/internal/service/skrevent/skr_events.go new file mode 100644 index 0000000000..aa89b5da9a --- /dev/null +++ b/internal/service/skrevent/skr_events.go @@ -0,0 +1,49 @@ +package skrevent + +import ( + "fmt" + "net/http" + "strings" + + watcherevent "github.com/kyma-project/runtime-watcher/listener/pkg/v2/event" + "github.com/kyma-project/runtime-watcher/listener/pkg/v2/types" + "sigs.k8s.io/controller-runtime/pkg/manager" + + "github.com/kyma-project/lifecycle-manager/pkg/security" +) + +// NewSKREventService creates a new SKR event service with a listener. +func NewSKREventService(mgr manager.Manager, listenerAddr, componentName string, enableDomainNameVerification bool) (*SkrRuntimeEventService, error) { + // Configure verification function + var verifyFunc watcherevent.Verify + if enableDomainNameVerification { + verifyFunc = security.NewRequestVerifier(mgr.GetClient()).Verify + } else { + verifyFunc = func(r *http.Request, watcherEvtObject *types.WatchEvent) error { + return nil + } + } + + // Create a new listener for this address + runnableListener := watcherevent.NewSKREventListener( + listenerAddr, + strings.ToLower(componentName), + verifyFunc, + ) + + // Add listener to manager as a runnable + err := mgr.Add(runnableListener) + if err != nil { + return nil, fmt.Errorf("failed to add listener to manager: %w", err) + } + + service := NewSkrRuntimeEventService(runnableListener) + + // Add service to manager for automatic lifecycle management + err = mgr.Add(service) + if err != nil { + return nil, fmt.Errorf("failed to add event service to manager: %w", err) + } + + return service, nil +} diff --git a/internal/service/skrevent/skr_events_test.go b/internal/service/skrevent/skr_events_test.go new file mode 100644 index 0000000000..fffd65e6b8 --- /dev/null +++ b/internal/service/skrevent/skr_events_test.go @@ -0,0 +1,27 @@ +package skrevent_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/kyma-project/lifecycle-manager/internal/service/skrevent" +) + +// Minimal tests to verify basic functionality. +func TestBasicServiceCreation(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + assert.NotNil(t, service, "Service should be created") +} + +func TestBasicServiceStop(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + err := service.Stop() + assert.NoError(t, err, "Stop should not error") +} + +func TestBasicEventSource(t *testing.T) { + service := skrevent.NewSkrRuntimeEventService(nil) + source := service.CreateEventSource(nil) + assert.NotNil(t, source, "EventSource should be created") +} diff --git a/internal/service/skrevent/typesconversion.go b/internal/service/skrevent/typesconversion.go new file mode 100644 index 0000000000..4e388a9599 --- /dev/null +++ b/internal/service/skrevent/typesconversion.go @@ -0,0 +1,51 @@ +package skrevent + +import ( + "errors" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// SKR Runtime event parsing errors. +var ( + ErrInvalidEventObject = errors.New("invalid event object type") + ErrMissingOwner = errors.New("missing owner in event") + ErrMissingWatched = errors.New("missing watched in event") + ErrInvalidOwnerFormat = errors.New("invalid owner format") + ErrInvalidWatchedFormat = errors.New("invalid watched format") +) + +func ExtractOwnerKey(eventObj *unstructured.Unstructured) (client.ObjectKey, error) { + ownerData, found := eventObj.Object["owner"] + if !found { + return client.ObjectKey{}, ErrMissingOwner + } + + owner, ok := ownerData.(map[string]interface{}) + if !ok { + return client.ObjectKey{}, ErrInvalidOwnerFormat + } + + name, _ := owner["name"].(string) + namespace, _ := owner["namespace"].(string) + + return client.ObjectKey{Name: name, Namespace: namespace}, nil +} + +func ExtractWatchedKey(eventObj *unstructured.Unstructured) (client.ObjectKey, error) { + watchedData, found := eventObj.Object["watched"] + if !found { + return client.ObjectKey{}, ErrMissingWatched + } + + watched, ok := watchedData.(map[string]interface{}) + if !ok { + return client.ObjectKey{}, ErrInvalidWatchedFormat + } + + name, _ := watched["name"].(string) + namespace, _ := watched["namespace"].(string) + + return client.ObjectKey{Name: name, Namespace: namespace}, nil +} diff --git a/internal/service/skrevent/typesconversion_internal_test.go b/internal/service/skrevent/typesconversion_internal_test.go new file mode 100644 index 0000000000..c2cd592178 --- /dev/null +++ b/internal/service/skrevent/typesconversion_internal_test.go @@ -0,0 +1,181 @@ +package skrevent + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestExtractOwnerKey(t *testing.T) { + tests := []struct { + name string + eventObj *unstructured.Unstructured + expected client.ObjectKey + expectError bool + errorType error + }{ + { + name: "valid owner extraction", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "owner": map[string]interface{}{ + "name": "test-kyma", + "namespace": "kyma-system", + }, + }, + }, + expected: client.ObjectKey{ + Name: "test-kyma", + Namespace: "kyma-system", + }, + expectError: false, + }, + { + name: "missing owner field", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "watched": map[string]interface{}{ + "name": "some-manifest", + }, + }, + }, + expectError: true, + errorType: ErrMissingOwner, + }, + { + name: "invalid owner format", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "owner": "invalid-string-format", + }, + }, + expectError: true, + errorType: ErrInvalidOwnerFormat, + }, + { + name: "cluster-scoped owner (empty namespace)", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "owner": map[string]interface{}{ + "name": "cluster-resource", + "namespace": "", + }, + }, + }, + expected: client.ObjectKey{ + Name: "cluster-resource", + Namespace: "", + }, + expectError: false, + }, + } + + for _, tcase := range tests { + t.Run(tcase.name, func(t *testing.T) { + result, err := ExtractOwnerKey(tcase.eventObj) + + if tcase.expectError { + require.Error(t, err) + if tcase.errorType != nil { + require.ErrorIs(t, err, tcase.errorType) + } + } else { + require.NoError(t, err) + assert.Equal(t, tcase.expected, result) + } + }) + } +} + +func TestExtractWatchedKey(t *testing.T) { + tests := []struct { + name string + eventObj *unstructured.Unstructured + expected client.ObjectKey + expectError bool + errorType error + }{ + { + name: "valid watched extraction", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "watched": map[string]interface{}{ + "name": "test-manifest", + "namespace": "default", + }, + }, + }, + expected: client.ObjectKey{ + Name: "test-manifest", + Namespace: "default", + }, + expectError: false, + }, + { + name: "missing watched field", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "owner": map[string]interface{}{ + "name": "some-kyma", + }, + }, + }, + expectError: true, + errorType: ErrMissingWatched, + }, + { + name: "invalid watched format", + eventObj: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "watched": []string{"invalid", "array", "format"}, + }, + }, + expectError: true, + errorType: ErrInvalidWatchedFormat, + }, + } + + for _, tcase := range tests { + t.Run(tcase.name, func(t *testing.T) { + result, err := ExtractWatchedKey(tcase.eventObj) + + if tcase.expectError { + require.Error(t, err) + if tcase.errorType != nil { + require.ErrorIs(t, err, tcase.errorType) + } + } else { + require.NoError(t, err) + assert.Equal(t, tcase.expected, result) + } + }) + } +} + +func TestBothExtractionFunctions(t *testing.T) { + eventObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "owner": map[string]interface{}{ + "name": "owner-kyma", + "namespace": "kyma-system", + }, + "watched": map[string]interface{}{ + "name": "watched-manifest", + "namespace": "default", + }, + }, + } + + ownerKey, err := ExtractOwnerKey(eventObj) + require.NoError(t, err) + assert.Equal(t, "owner-kyma", ownerKey.Name) + assert.Equal(t, "kyma-system", ownerKey.Namespace) + + watchedKey, err := ExtractWatchedKey(eventObj) + require.NoError(t, err) + assert.Equal(t, "watched-manifest", watchedKey.Name) + assert.Equal(t, "default", watchedKey.Namespace) +} diff --git a/pkg/security/san_pinning.go b/pkg/security/san_pinning.go index f089fd9a9d..eab87af1c7 100644 --- a/pkg/security/san_pinning.go +++ b/pkg/security/san_pinning.go @@ -11,7 +11,7 @@ import ( "strings" "github.com/go-logr/logr" - "github.com/kyma-project/runtime-watcher/listener/pkg/types" + "github.com/kyma-project/runtime-watcher/listener/pkg/v2/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" diff --git a/tests/integration/security/san_pinning_helper_test.go b/tests/integration/security/san_pinning_helper_test.go index f1f712c6da..ecd6d08fd3 100644 --- a/tests/integration/security/san_pinning_helper_test.go +++ b/tests/integration/security/san_pinning_helper_test.go @@ -5,14 +5,12 @@ import ( "net/http" "strings" - "github.com/kyma-project/runtime-watcher/listener/pkg/types" - apicorev1 "k8s.io/api/core/v1" - apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "github.com/kyma-project/lifecycle-manager/api/shared" "github.com/kyma-project/lifecycle-manager/api/v1beta2" "github.com/kyma-project/lifecycle-manager/pkg/security" + "github.com/kyma-project/runtime-watcher/listener/pkg/v2/types" + apicorev1 "k8s.io/api/core/v1" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func createKyma(kymaName string, annotations map[string]string) *v1beta2.Kyma { @@ -53,11 +51,11 @@ func createRequest(kymaName string, header map[string][]string) *http.Request { func createWatcherCR(kymaName string) *types.WatchEvent { return &types.WatchEvent{ - Owner: client.ObjectKey{ + Owner: types.ObjectKey{ Namespace: "default", Name: kymaName, }, - Watched: client.ObjectKey{ + Watched: types.ObjectKey{ Namespace: "default", Name: kymaName, }, diff --git a/tests/integration/security/san_pinning_test.go b/tests/integration/security/san_pinning_test.go index 6b22f9dccd..68de4e6ff3 100644 --- a/tests/integration/security/san_pinning_test.go +++ b/tests/integration/security/san_pinning_test.go @@ -9,7 +9,7 @@ import ( "testing" "github.com/go-logr/zapr" - "github.com/kyma-project/runtime-watcher/listener/pkg/types" + "github.com/kyma-project/runtime-watcher/listener/pkg/v2/types" "github.com/stretchr/testify/require" "go.uber.org/zap" @@ -22,10 +22,12 @@ import ( func TestRequestVerifier_verifySAN(t *testing.T) { t.Parallel() + type args struct { certificate *x509.Certificate kymaDomain string } + tests := []struct { name string args args @@ -87,14 +89,17 @@ func TestRequestVerifier_verifySAN(t *testing.T) { zapLog, err := zap.NewDevelopment() require.NoError(t, err) + verifier := &security.RequestVerifier{ Client: nil, Log: zapr.NewLogger(zapLog), } + for _, tt := range tests { test := tt t.Run(test.name, func(t *testing.T) { t.Parallel() + got, err := verifier.VerifySAN(test.args.certificate, test.args.kymaDomain) require.NoError(t, err) require.Equal(t, test.want, got)