From 7bd80ba70721ff338b24681d5b4da06f1eadce82 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Thu, 18 Dec 2025 19:33:31 +0300 Subject: [PATCH 01/11] [feature] application hook Signed-off-by: Stepan Paksashvili --- examples/README.md | 4 +- examples/single-file-app-example/README.md | 35 ++ examples/single-file-app-example/hooks/go.mod | 98 ++++++ examples/single-file-app-example/hooks/go.sum | 301 ++++++++++++++++++ .../single-file-app-example/hooks/main.go | 55 ++++ .../hooks/main_test.go | 56 ++++ .../hooks/suite_test.go | 29 ++ examples/single-file-example/hooks/main.go | 4 +- .../single-file-example/hooks/main_test.go | 2 +- internal/common-hooks/readiness/hook.go | 2 +- internal/controller/controller.go | 41 +-- internal/executor/application.go | 118 +++++++ internal/executor/executor.go | 45 +++ .../executor_test.go} | 33 +- internal/executor/module.go | 108 +++++++ internal/executor/registry/registry.go | 53 +++ .../request_mock_test.go} | 4 +- internal/hook/go_hook.go | 154 --------- .../object-patch/object_patch_collector.go | 187 ----------- internal/object-patch/operation_types.go | 28 -- internal/object-patch/patch.go | 25 -- internal/objectpatch/collector.go | 196 ++++++++++++ internal/objectpatch/namespaced.go | 100 ++++++ internal/objectpatch/operation.go | 32 ++ internal/objectpatch/patch.go | 43 +++ .../snapshot.go} | 0 .../snapshot_test.go} | 2 +- internal/registry/registry.go | 51 --- internal/transport/file/transport.go | 12 +- pkg/dependency.go | 9 + pkg/hook.go | 47 ++- pkg/object-patch/patch_options.go | 5 + pkg/patch.go | 47 ++- pkg/registry/registry.go | 89 +++--- pkg/registry/registry_test.go | 58 ++-- 35 files changed, 1491 insertions(+), 582 deletions(-) create mode 100644 examples/single-file-app-example/README.md create mode 100644 examples/single-file-app-example/hooks/go.mod create mode 100644 examples/single-file-app-example/hooks/go.sum create mode 100644 examples/single-file-app-example/hooks/main.go create mode 100644 examples/single-file-app-example/hooks/main_test.go create mode 100644 examples/single-file-app-example/hooks/suite_test.go create mode 100644 internal/executor/application.go create mode 100644 internal/executor/executor.go rename internal/{hook/go_hook_test.go => executor/executor_test.go} (90%) create mode 100644 internal/executor/module.go create mode 100644 internal/executor/registry/registry.go rename internal/{hook/hook_request_mock_test.go => executor/request_mock_test.go} (99%) delete mode 100644 internal/hook/go_hook.go delete mode 100644 internal/object-patch/object_patch_collector.go delete mode 100644 internal/object-patch/operation_types.go delete mode 100644 internal/object-patch/patch.go create mode 100644 internal/objectpatch/collector.go create mode 100644 internal/objectpatch/namespaced.go create mode 100644 internal/objectpatch/operation.go create mode 100644 internal/objectpatch/patch.go rename internal/{object-patch/object_filter.go => objectpatch/snapshot.go} (100%) rename internal/{object-patch/object_filter_test.go => objectpatch/snapshot_test.go} (97%) delete mode 100644 internal/registry/registry.go diff --git a/examples/README.md b/examples/README.md index 8854ec0a..03c2e2f0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -12,4 +12,6 @@ [Dockerfile and Makefile for building](https://github.com/deckhouse/module-sdk/tree/main/examples/scripts) -[Module hook example with settings checking](https://github.com/deckhouse/module-sdk/tree/main/examples/settings-check) \ No newline at end of file +[Settings check example](https://github.com/deckhouse/module-sdk/tree/main/examples/settings-check) + +[Application hook single file example](https://github.com/deckhouse/module-sdk/tree/main/examples/single-file-application-example) \ No newline at end of file diff --git a/examples/single-file-app-example/README.md b/examples/single-file-app-example/README.md new file mode 100644 index 00000000..24b2f689 --- /dev/null +++ b/examples/single-file-app-example/README.md @@ -0,0 +1,35 @@ +# Single File Application Hooks Example +In this example you can build binary from one file. + +### Run + +To get list of your registered hooks +```bash +go run . hook list +``` + +To get configs of your registered hooks +```bash +go run . hook config +``` + +To dump configs of your registered hooks in file +```bash +go run . hook dump +``` + +To run registered hook with index '0' (you can see index of your hook in output of "hook list" command) +```bash +go run . hook run 0 +``` + +By default, all logs in hooks are suppressed and he waiting for files in default folders. +To make them available, you must add env variable LOG_LEVEL and CREATE_FILES. +```bash +CREATE_FILES=true LOG_LEVEL=INFO go run . hook run 0 +``` + +### Build +```bash +go build -o example-application-hooks . +``` \ No newline at end of file diff --git a/examples/single-file-app-example/hooks/go.mod b/examples/single-file-app-example/hooks/go.mod new file mode 100644 index 00000000..6d7a0f4b --- /dev/null +++ b/examples/single-file-app-example/hooks/go.mod @@ -0,0 +1,98 @@ +module singlefileexample + +go 1.24 + +require ( + github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250814094423-e9f108b41a1a + github.com/deckhouse/module-sdk v0.0.0 + github.com/onsi/ginkgo v1.16.5 + github.com/onsi/gomega v1.36.1 + k8s.io/apimachinery v0.32.10 +) + +require ( + github.com/DataDog/gostackparse v0.7.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v11 v11.3.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/docker/cli v28.2.2+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.3 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/ettle/strcase v0.2.0 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/gojuno/minimock/v3 v3.4.7 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nxadm/tail v1.4.8 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.9.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/sylabs/oci-tools v0.16.0 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/vbatts/tar-split v0.12.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + golang.org/x/net v0.36.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.8.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.3 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + gotest.tools/v3 v3.5.1 // indirect + k8s.io/api v0.32.10 // indirect + k8s.io/apiextensions-apiserver v0.32.10 // indirect + k8s.io/client-go v0.32.10 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect + k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect + sigs.k8s.io/controller-runtime v0.20.4 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) + +replace github.com/deckhouse/deckhouse/dhctl => github.com/deckhouse/deckhouse/dhctl v0.0.0-20241122092255-d267ec38bcdd + +replace github.com/deckhouse/module-sdk => ../../../ diff --git a/examples/single-file-app-example/hooks/go.sum b/examples/single-file-app-example/hooks/go.sum new file mode 100644 index 00000000..80c89820 --- /dev/null +++ b/examples/single-file-app-example/hooks/go.sum @@ -0,0 +1,301 @@ +github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= +github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caarlos0/env/v11 v11.3.1 h1:cArPWC15hWmEt+gWk7YBi7lEXTXCvpaSdCiZE2X5mCA= +github.com/caarlos0/env/v11 v11.3.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250814094423-e9f108b41a1a h1:An8WnJ2wm42Rr6fbhNusLho6KNfPAYV+nuitsEHNXSE= +github.com/deckhouse/deckhouse/pkg/log v0.0.0-20250814094423-e9f108b41a1a/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw= +github.com/docker/cli v28.2.2+incompatible h1:qzx5BNUDFqlvyq4AHzdNB7gSyVTmU4cgsyN9SdInc1A= +github.com/docker/cli v28.2.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= +github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= +github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/gojuno/minimock/v3 v3.4.7 h1:vhE5zpniyPDRT0DXd5s3DbtZJVlcbmC5k80izYtj9lY= +github.com/gojuno/minimock/v3 v3.4.7/go.mod h1:QxJk4mdPrVyYUmEZGc2yD2NONpqM/j4dWhsy9twjFHg= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.6 h1:cvWX87UxxLgaH76b4hIvya6Dzz9qHB31qAwjAohdSTU= +github.com/google/go-containerregistry v0.20.6/go.mod h1:T0x8MuoAoKX/873bkeSfLD2FAkwCDf9/HZgsFJ02E2Y= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= +github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= +github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg= +github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw= +github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.5 h1:rx1mwF95RxZ3/83sdS4Yp7t2C5TCokvWP4TBRbAyEWY= +github.com/sebdah/goldie/v2 v2.5.5/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/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/sylabs/oci-tools v0.16.0 h1:4pdwS7HtNT9Y+3jpwNQo590Vj5218vbsestGilgSVtA= +github.com/sylabs/oci-tools v0.16.0/go.mod h1:278n9ttZ0B9vTwbQ4896HCwwgZf3DvU82XD5wS+fZwI= +github.com/sylabs/sif/v2 v2.19.1 h1:1eeMmFc8elqJe60ZiWwXgL3gMheb0IP4GmNZ4q0IEA0= +github.com/sylabs/sif/v2 v2.19.1/go.mod h1:U1SUhvl8X1JIxAylC0DYz1fa/Xba6EMZD1dGPGBH83E= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/vbatts/tar-split v0.12.1 h1:CqKoORW7BUWBe7UL/iqTVvkTBOF8UvOMKOIZykxnnbo= +github.com/vbatts/tar-split v0.12.1/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA= +golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= +golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= +gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +k8s.io/api v0.32.10 h1:ocp4turNfa1V40TuBW/LuA17TeXG9g/GI2ebg0KxBNk= +k8s.io/api v0.32.10/go.mod h1:AsMsc4b6TuampYqgMEGSv0HBFpRS4BlKTXAVCAa7oF4= +k8s.io/apiextensions-apiserver v0.32.10 h1:mAZT8fX/jM9pl7qWkFhhsjQZ8ZkmAhEivfUNw8uKXmo= +k8s.io/apiextensions-apiserver v0.32.10/go.mod h1:wEvqU9kFUQOYminqrroY6+fvSs6iMb7QiiFmcN3b6KY= +k8s.io/apimachinery v0.32.10 h1:SAg2kUPLYRcBJQj66oniP1BnXSqw+l1GvJFsJlBmVvQ= +k8s.io/apimachinery v0.32.10/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.10 h1:MFmIjsKtcnn7mStjrJG1ZW2WzLsKKn6ZtL9hHM/W0xU= +k8s.io/client-go v0.32.10/go.mod h1:qJy/Ws3zSwnu/nD75D+/of1uxbwWHxrYT5P3FuobVLI= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= +k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= +sigs.k8s.io/controller-runtime v0.20.4/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= +sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/examples/single-file-app-example/hooks/main.go b/examples/single-file-app-example/hooks/main.go new file mode 100644 index 00000000..9f3d5cd7 --- /dev/null +++ b/examples/single-file-app-example/hooks/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "log/slog" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/app" + objectpatch "github.com/deckhouse/module-sdk/pkg/object-patch" + "github.com/deckhouse/module-sdk/pkg/registry" +) + +const ( + SnapshotKey = "apiservers" +) + +var _ = registry.RegisterFunc(config, Handle) + +var config = &pkg.HookConfig{ + Kubernetes: []pkg.KubernetesConfig{ + { + Name: SnapshotKey, + APIVersion: "metav1", + Kind: "Pod", + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{"kube-system"}, + }, + }, + LabelSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"component": "kube-apiserver"}, + }, + JqFilter: ".metadata.name", + }, + }, +} + +func Handle(_ context.Context, input *pkg.ApplicationHookInput) error { + podNames, err := objectpatch.UnmarshalToStruct[string](input.Snapshots, SnapshotKey) + if err != nil { + return err + } + + input.Logger.Info("found apiserver pods", slog.Any("podNames", podNames)) + + input.Values.Set("test.internal.apiServers", podNames) + + return nil +} + +func main() { + app.Run() +} diff --git a/examples/single-file-app-example/hooks/main_test.go b/examples/single-file-app-example/hooks/main_test.go new file mode 100644 index 00000000..8fc9e9fb --- /dev/null +++ b/examples/single-file-app-example/hooks/main_test.go @@ -0,0 +1,56 @@ +package main_test + +import ( + "context" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/testing/mock" + + singlefileexample "singlefileexample" +) + +const ( + firstSnapshot = "one" + secondSnapshot = "two" +) + +var _ = Describe("handle hook single file example", func() { + snapshots := mock.NewSnapshotsMock(GinkgoT()) + snapshots.GetMock.When(singlefileexample.SnapshotKey).Then( + []pkg.Snapshot{ + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + str := v.(*string) + *str = firstSnapshot + + return nil + }), + mock.NewSnapshotMock(GinkgoT()).UnmarshalToMock.Set(func(v any) error { + str := v.(*string) + *str = secondSnapshot + + return nil + }), + }, + ) + + values := mock.NewOutputPatchableValuesCollectorMock(GinkgoT()) + values.SetMock.When("test.internal.apiServers", []string{firstSnapshot, secondSnapshot}) + + var input = &pkg.ApplicationHookInput{ + Snapshots: snapshots, + Values: values, + Logger: log.NewNop(), + } + + Context("reconcile func", func() { + It("reconcile func executed correctly", func() { + err := singlefileexample.Handle(context.Background(), input) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) +}) diff --git a/examples/single-file-app-example/hooks/suite_test.go b/examples/single-file-app-example/hooks/suite_test.go new file mode 100644 index 00000000..784c3128 --- /dev/null +++ b/examples/single-file-app-example/hooks/suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2021 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func Test_Suite(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "") +} diff --git a/examples/single-file-example/hooks/main.go b/examples/single-file-example/hooks/main.go index f664de74..ba5da930 100644 --- a/examples/single-file-example/hooks/main.go +++ b/examples/single-file-example/hooks/main.go @@ -19,7 +19,7 @@ const ( SnapshotKey = "apiservers" ) -var _ = registry.RegisterFunc(config, HandlerHook) +var _ = registry.RegisterFunc(config, Handle) var config = &pkg.HookConfig{ Kubernetes: []pkg.KubernetesConfig{ @@ -40,7 +40,7 @@ var config = &pkg.HookConfig{ }, } -func HandlerHook(_ context.Context, input *pkg.HookInput) error { +func Handle(_ context.Context, input *pkg.HookInput) error { podNames, err := objectpatch.UnmarshalToStruct[string](input.Snapshots, "apiservers") if err != nil { return err diff --git a/examples/single-file-example/hooks/main_test.go b/examples/single-file-example/hooks/main_test.go index c87c89e4..f62b0320 100644 --- a/examples/single-file-example/hooks/main_test.go +++ b/examples/single-file-example/hooks/main_test.go @@ -49,7 +49,7 @@ var _ = Describe("handle hook single file example", func() { Context("refoncile func", func() { It("reconcile func executed correctly", func() { - err := singlefileexample.HandlerHook(context.Background(), input) + err := singlefileexample.Handle(context.Background(), input) Expect(err).ShouldNot(HaveOccurred()) }) }) diff --git a/internal/common-hooks/readiness/hook.go b/internal/common-hooks/readiness/hook.go index 65c3f7e6..843be249 100644 --- a/internal/common-hooks/readiness/hook.go +++ b/internal/common-hooks/readiness/hook.go @@ -46,7 +46,7 @@ type ReadinessHookConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -func NewReadinessHookEM(cfg *ReadinessHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { +func NewReadinessHookEM(cfg *ReadinessHookConfig) (*pkg.HookConfig, pkg.HookFunc[*pkg.HookInput]) { if cfg == nil { panic("empty readiness config") } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 96a405a6..39ba143e 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -12,18 +12,18 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/module-sdk/internal/common-hooks/readiness" - "github.com/deckhouse/module-sdk/internal/registry" + execregistry "github.com/deckhouse/module-sdk/internal/executor/registry" "github.com/deckhouse/module-sdk/internal/transport/file" "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/dependency" gohook "github.com/deckhouse/module-sdk/pkg/hook" - outerRegistry "github.com/deckhouse/module-sdk/pkg/registry" + hookregistry "github.com/deckhouse/module-sdk/pkg/registry" "github.com/deckhouse/module-sdk/pkg/settingscheck" "github.com/deckhouse/module-sdk/pkg/utils/ptr" ) type HookController struct { - registry *registry.HookRegistry + registry *execregistry.Registry fConfig *file.Config settingsCheck settingscheck.Check @@ -40,8 +40,9 @@ type HookSender interface { } func NewHookController(cfg *Config, logger *log.Logger) *HookController { - reg := registry.NewHookRegistry(logger) - reg.Add(outerRegistry.Registry().Hooks()...) + reg := execregistry.NewRegistry(logger) + reg.RegisterModuleHooks(hookregistry.Registry().ModuleHooks()...) + reg.RegisterAppHooks(hookregistry.Registry().ApplicationHooks()...) if cfg.ReadinessConfig != nil { addReadinessHook(reg, cfg.ReadinessConfig) @@ -56,7 +57,7 @@ func NewHookController(cfg *Config, logger *log.Logger) *HookController { } } -func addReadinessHook(reg *registry.HookRegistry, cfg *ReadinessConfig) { +func addReadinessHook(reg *execregistry.Registry, cfg *ReadinessConfig) { readinessConfig := &readiness.ReadinessHookConfig{ ModuleName: cfg.ModuleName, IntervalInSeconds: cfg.IntervalInSeconds, @@ -67,15 +68,15 @@ func addReadinessHook(reg *registry.HookRegistry, cfg *ReadinessConfig) { config.Metadata.Name = "readiness" config.Metadata.Path = "common-hooks/readiness" - reg.SetReadinessHook(&pkg.Hook{Config: config, ReconcileFunc: f}) + reg.SetReadinessHook(pkg.Hook[*pkg.HookInput]{Config: config, HookFunc: f}) } func (c *HookController) ListHooksMeta() []pkg.HookMetadata { - hooks := c.registry.Hooks() + hooks := c.registry.Executors() hooksmetas := make([]pkg.HookMetadata, 0, len(hooks)) for _, hook := range hooks { - hooksmetas = append(hooksmetas, hook.GetConfig().Metadata) + hooksmetas = append(hooksmetas, hook.Config().Metadata) } return hooksmetas @@ -85,7 +86,7 @@ func (c *HookController) ListHooksMeta() []pkg.HookMetadata { var ErrHookIndexIsNotExists = errors.New("hook index does not exist") func (c *HookController) RunHook(ctx context.Context, idx int) error { - hooks := c.registry.Hooks() + hooks := c.registry.Executors() if len(hooks) <= idx { return ErrHookIndexIsNotExists @@ -93,7 +94,7 @@ func (c *HookController) RunHook(ctx context.Context, idx int) error { hook := hooks[idx] - transport := file.NewTransport(c.fConfig, hook.GetName(), c.dc, c.logger.Named("file-transport")) + transport := file.NewTransport(c.fConfig, hook.Config().Metadata.Name, c.dc, c.logger.Named("file-transport")) hookRes, err := hook.Execute(ctx, transport.NewRequest()) if err != nil { @@ -126,7 +127,7 @@ func (c *HookController) RunReadiness(ctx context.Context) error { return ErrReadinessHookDoesNotExists } - transport := file.NewTransport(c.fConfig, hook.GetName(), c.dc, c.logger.Named("file-transport")) + transport := file.NewTransport(c.fConfig, hook.Config().Metadata.Name, c.dc, c.logger.Named("file-transport")) hookRes, err := hook.Execute(ctx, transport.NewRequest()) if err != nil { @@ -167,14 +168,14 @@ func (c *HookController) CheckSettings(ctx context.Context) error { var ErrNoHooksRegistered = errors.New("no hooks registered") func (c *HookController) PrintHookConfigs() error { - if len(c.registry.Hooks()) == 0 && c.settingsCheck == nil { + if len(c.registry.Executors()) == 0 && c.settingsCheck == nil { return ErrNoHooksRegistered } configs := make([]gohook.HookConfig, 0, 1) - for _, hook := range c.registry.Hooks() { - configs = append(configs, *remapHookConfigToHookConfig(hook.GetConfig())) + for _, hook := range c.registry.Executors() { + configs = append(configs, *remapHookConfigToHookConfig(hook.Config())) } cfg := &gohook.BatchHookConfig{ @@ -183,7 +184,7 @@ func (c *HookController) PrintHookConfigs() error { } if c.registry.Readiness() != nil { - cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().GetConfig()) + cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().Config()) } if c.settingsCheck != nil { @@ -202,7 +203,7 @@ func (c *HookController) PrintHookConfigs() error { } func (c *HookController) WriteHookConfigsInFile() error { - if len(c.registry.Hooks()) == 0 { + if len(c.registry.Executors()) == 0 { return ErrNoHooksRegistered } @@ -227,8 +228,8 @@ func (c *HookController) WriteHookConfigsInFile() error { configs := make([]gohook.HookConfig, 0, 1) - for _, hook := range c.registry.Hooks() { - configs = append(configs, *remapHookConfigToHookConfig(hook.GetConfig())) + for _, hook := range c.registry.Executors() { + configs = append(configs, *remapHookConfigToHookConfig(hook.Config())) } cfg := &gohook.BatchHookConfig{ @@ -237,7 +238,7 @@ func (c *HookController) WriteHookConfigsInFile() error { } if c.registry.Readiness() != nil { - cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().GetConfig()) + cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().Config()) } err = json.NewEncoder(f).Encode(cfg) diff --git a/internal/executor/application.go b/internal/executor/application.go new file mode 100644 index 00000000..1d25af34 --- /dev/null +++ b/internal/executor/application.go @@ -0,0 +1,118 @@ +package executor + +import ( + "context" + "fmt" + "log/slog" + "os" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/internal/metric" + "github.com/deckhouse/module-sdk/internal/objectpatch" + "github.com/deckhouse/module-sdk/pkg" + patchablevalues "github.com/deckhouse/module-sdk/pkg/patchable-values" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +type applicationExecutor struct { + hook pkg.Hook[*pkg.ApplicationHookInput] + logger *log.Logger +} + +// NewApplicationExecutor creates a new application executor +func NewApplicationExecutor(h pkg.Hook[*pkg.ApplicationHookInput], logger *log.Logger) Executor { + return &applicationExecutor{ + hook: h, + logger: logger, + } +} + +func (e *applicationExecutor) Config() *pkg.HookConfig { + return e.hook.Config +} + +func (e *applicationExecutor) Execute(ctx context.Context, req Request) (Result, error) { + // Values are patched in-place, so an error can occur. + rawValues, err := req.GetValues() + if err != nil { + e.logger.Error("get values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get values: %w", err) + } + + patchableValues, err := patchablevalues.NewPatchableValues(rawValues) + if err != nil { + e.logger.Error("new patchable values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get patchable values: %w", err) + } + + bContext, err := req.GetBindingContexts() + if err != nil { + e.logger.Warn("get binding context", slog.String("error", err.Error())) + } + + formattedSnapshots := make(objectpatch.Snapshots, len(bContext)) + for _, bc := range bContext { + for snapBindingName, snaps := range bc.Snapshots { + for _, snap := range snaps { + if snap.FilterResult != nil { + formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.FilterResult)) + + continue + } + + if snap.Object != nil { + formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.Object)) + + continue + } + } + } + } + + inst := newAppInstance() + + metricsCollector := metric.NewCollector() + namespacedPatchCollector := objectpatch.NewNamespacedCollector(inst.namespace, e.logger.Named("object-patch-collector")) + + err = e.hook.HookFunc(ctx, &pkg.ApplicationHookInput{ + Snapshots: formattedSnapshots, + Instance: newAppInstance(), + Values: patchableValues, + PatchCollector: namespacedPatchCollector, + MetricsCollector: metricsCollector, + DC: req.GetDependencyContainer(), + Logger: e.logger, + }) + if err != nil { + return nil, fmt.Errorf("hook reconcile func: %w", err) + } + + return &result{ + patches: map[utils.ValuesPatchType]pkg.Outputer{ + utils.MemoryValuesPatch: patchableValues, + }, + objectPatchCollector: namespacedPatchCollector, + metricsCollector: metricsCollector, + }, nil +} + +type applicationInstance struct { + name string + namespace string +} + +func newAppInstance() *applicationInstance { + return &applicationInstance{ + name: os.Getenv(pkg.EnvApplicationName), + namespace: os.Getenv(pkg.EnvApplicationNamespace), + } +} + +func (i *applicationInstance) Name() string { + return i.name +} + +func (i *applicationInstance) Namespace() string { + return i.namespace +} diff --git a/internal/executor/executor.go b/internal/executor/executor.go new file mode 100644 index 00000000..7f2331e1 --- /dev/null +++ b/internal/executor/executor.go @@ -0,0 +1,45 @@ +package executor + +import ( + "context" + + bctx "github.com/deckhouse/module-sdk/internal/binding-context" + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +type Executor interface { + Config() *pkg.HookConfig + Execute(ctx context.Context, req Request) (Result, error) +} + +type Request interface { + GetValues() (map[string]any, error) + GetConfigValues() (map[string]any, error) + GetBindingContexts() ([]bctx.BindingContext, error) + GetDependencyContainer() pkg.DependencyContainer +} + +type Result interface { + MetricsCollector() pkg.Outputer + ObjectPatchCollector() pkg.Outputer + ValuesPatchCollector(key utils.ValuesPatchType) pkg.Outputer +} + +type result struct { + objectPatchCollector pkg.Outputer + metricsCollector pkg.Outputer + patches map[utils.ValuesPatchType]pkg.Outputer +} + +func (r *result) MetricsCollector() pkg.Outputer { + return r.metricsCollector +} + +func (r *result) ObjectPatchCollector() pkg.Outputer { + return r.objectPatchCollector +} + +func (r *result) ValuesPatchCollector(key utils.ValuesPatchType) pkg.Outputer { + return r.patches[key] +} diff --git a/internal/hook/go_hook_test.go b/internal/executor/executor_test.go similarity index 90% rename from internal/hook/go_hook_test.go rename to internal/executor/executor_test.go index 4c44d8e7..55cb2319 100644 --- a/internal/hook/go_hook_test.go +++ b/internal/executor/executor_test.go @@ -1,4 +1,4 @@ -package hook_test +package executor_test import ( "context" @@ -10,7 +10,7 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" bindingcontext "github.com/deckhouse/module-sdk/internal/binding-context" - "github.com/deckhouse/module-sdk/internal/hook" + "github.com/deckhouse/module-sdk/internal/executor" "github.com/deckhouse/module-sdk/pkg" ) @@ -23,7 +23,7 @@ func Test_Go_Hook_Execute(t *testing.T) { } type fields struct { - setupHookRequest func(t *testing.T) hook.HookRequest + setupHookRequest func(t *testing.T) executor.Request setupHookReconcileFunc func(t *testing.T) func(ctx context.Context, input *pkg.HookInput) error } @@ -46,7 +46,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -97,7 +97,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -148,7 +148,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -200,7 +200,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -225,7 +225,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -252,7 +252,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -280,7 +280,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -310,7 +310,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -344,7 +344,7 @@ func Test_Go_Hook_Execute(t *testing.T) { enabled: true, }, fields: fields{ - setupHookRequest: func(t *testing.T) hook.HookRequest { + setupHookRequest: func(t *testing.T) executor.Request { hr := NewHookRequestMock(t) vals := hr.GetValuesMock.Expect() @@ -382,11 +382,14 @@ func Test_Go_Hook_Execute(t *testing.T) { t.Run(tt.meta.name, func(t *testing.T) { t.Parallel() - cfg := &pkg.HookConfig{} + h := pkg.Hook[*pkg.HookInput]{ + Config: new(pkg.HookConfig), + HookFunc: tt.fields.setupHookReconcileFunc(t), + } - h := hook.NewHook(cfg, tt.fields.setupHookReconcileFunc(t)).SetLogger(log.NewNop()) + exec := executor.NewModuleExecutor(h, log.NewNop()) - _, err := h.Execute(context.Background(), tt.fields.setupHookRequest(t)) + _, err := exec.Execute(context.Background(), tt.fields.setupHookRequest(t)) if tt.wants.err != "" { assert.Contains(t, err.Error(), tt.wants.err) } else { diff --git a/internal/executor/module.go b/internal/executor/module.go new file mode 100644 index 00000000..f54b7014 --- /dev/null +++ b/internal/executor/module.go @@ -0,0 +1,108 @@ +package executor + +import ( + "context" + "fmt" + "log/slog" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/internal/metric" + "github.com/deckhouse/module-sdk/internal/objectpatch" + "github.com/deckhouse/module-sdk/pkg" + patchablevalues "github.com/deckhouse/module-sdk/pkg/patchable-values" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +type moduleExecutor struct { + hook pkg.Hook[*pkg.HookInput] + logger *log.Logger +} + +// NewModuleExecutor creates a new application hook +func NewModuleExecutor(h pkg.Hook[*pkg.HookInput], logger *log.Logger) Executor { + return &moduleExecutor{ + hook: h, + logger: logger, + } +} + +func (e *moduleExecutor) Config() *pkg.HookConfig { + return e.hook.Config +} + +func (e *moduleExecutor) Execute(ctx context.Context, req Request) (Result, error) { + // Values are patched in-place, so an error can occur. + rawValues, err := req.GetValues() + if err != nil { + e.logger.Error("get values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get values: %w", err) + } + + patchableValues, err := patchablevalues.NewPatchableValues(rawValues) + if err != nil { + e.logger.Error("new patchable values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get patchable values: %w", err) + } + + rawConfigValues, err := req.GetConfigValues() + if err != nil { + e.logger.Error("get config values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get config values: %w", err) + } + + patchableConfigValues, err := patchablevalues.NewPatchableValues(rawConfigValues) + if err != nil { + e.logger.Error("new patchable config values", slog.String("error", err.Error())) + return nil, fmt.Errorf("get patchable config values: %w", err) + } + + bContext, err := req.GetBindingContexts() + if err != nil { + e.logger.Warn("get binding context", slog.String("error", err.Error())) + } + + formattedSnapshots := make(objectpatch.Snapshots, len(bContext)) + for _, bc := range bContext { + for snapBindingName, snaps := range bc.Snapshots { + for _, snap := range snaps { + if snap.FilterResult != nil { + formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.FilterResult)) + + continue + } + + if snap.Object != nil { + formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.Object)) + + continue + } + } + } + } + + metricsCollector := metric.NewCollector() + objectPatchCollector := objectpatch.NewCollector(e.logger.Named("object-patch-collector")) + + err = e.hook.HookFunc(ctx, &pkg.HookInput{ + Snapshots: formattedSnapshots, + Values: patchableValues, + ConfigValues: patchableConfigValues, + PatchCollector: objectPatchCollector, + MetricsCollector: metricsCollector, + DC: req.GetDependencyContainer(), + Logger: e.logger, + }) + if err != nil { + return nil, fmt.Errorf("hook reconcile func: %w", err) + } + + return &result{ + patches: map[utils.ValuesPatchType]pkg.Outputer{ + utils.MemoryValuesPatch: patchableValues, + utils.ConfigMapPatch: patchableConfigValues, + }, + objectPatchCollector: objectPatchCollector, + metricsCollector: metricsCollector, + }, nil +} diff --git a/internal/executor/registry/registry.go b/internal/executor/registry/registry.go new file mode 100644 index 00000000..ac1c097d --- /dev/null +++ b/internal/executor/registry/registry.go @@ -0,0 +1,53 @@ +package registry + +import ( + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/internal/executor" + "github.com/deckhouse/module-sdk/pkg" +) + +type Registry struct { + executors []executor.Executor + readinessExecutor executor.Executor + + logger *log.Logger +} + +func NewRegistry(logger *log.Logger) *Registry { + return &Registry{ + executors: make([]executor.Executor, 0, 1), + logger: logger, + } +} + +// Executors returns all executors +func (r *Registry) Executors() []executor.Executor { + return r.executors +} + +// Readiness returns the readiness hook +// It is used to check if the module is ready to serve requests +// It is not used for the readiness probe +// The readiness probe is implemented in the module itself +func (r *Registry) Readiness() executor.Executor { + return r.readinessExecutor +} + +func (r *Registry) RegisterModuleHooks(hooks ...pkg.Hook[*pkg.HookInput]) { + for _, h := range hooks { + exec := executor.NewModuleExecutor(h, r.logger.Named(h.Config.Metadata.Name)) + r.executors = append(r.executors, exec) + } +} + +func (r *Registry) RegisterAppHooks(hooks ...pkg.Hook[*pkg.ApplicationHookInput]) { + for _, h := range hooks { + exec := executor.NewApplicationExecutor(h, r.logger.Named(h.Config.Metadata.Name)) + r.executors = append(r.executors, exec) + } +} + +func (r *Registry) SetReadinessHook(h pkg.Hook[*pkg.HookInput]) { + r.readinessExecutor = executor.NewModuleExecutor(h, r.logger.Named(h.Config.Metadata.Name)) +} diff --git a/internal/hook/hook_request_mock_test.go b/internal/executor/request_mock_test.go similarity index 99% rename from internal/hook/hook_request_mock_test.go rename to internal/executor/request_mock_test.go index 44ec4c24..6e0bb1b2 100644 --- a/internal/hook/hook_request_mock_test.go +++ b/internal/executor/request_mock_test.go @@ -1,8 +1,8 @@ // Code generated by http://github.com/gojuno/minimock (v3.4.7). DO NOT EDIT. -package hook_test +package executor_test -//go:generate minimock -i github.com/deckhouse/module-sdk/internal/hook.HookRequest -o hook_request_mock_test.go -n HookRequestMock -p hook_test +//go:generate minimock -i github.com/deckhouse/module-sdk/internal/executor.Request -o hook_request_mock_test.go -n HookRequestMock -p hook_test import ( "sync" diff --git a/internal/hook/go_hook.go b/internal/hook/go_hook.go deleted file mode 100644 index 6c351107..00000000 --- a/internal/hook/go_hook.go +++ /dev/null @@ -1,154 +0,0 @@ -package hook - -import ( - "context" - "fmt" - "log/slog" - - "github.com/deckhouse/deckhouse/pkg/log" - - bindingcontext "github.com/deckhouse/module-sdk/internal/binding-context" - metric "github.com/deckhouse/module-sdk/internal/metric" - objectpatch "github.com/deckhouse/module-sdk/internal/object-patch" - "github.com/deckhouse/module-sdk/pkg" - patchablevalues "github.com/deckhouse/module-sdk/pkg/patchable-values" - "github.com/deckhouse/module-sdk/pkg/utils" -) - -type Hook struct { - config *pkg.HookConfig - reconcileFunc pkg.ReconcileFunc - - logger *log.Logger -} - -// NewHook creates a new go hook -func NewHook(config *pkg.HookConfig, f pkg.ReconcileFunc) *Hook { - logger := log.NewLogger() - - return &Hook{ - config: config, - reconcileFunc: f, - logger: logger.Named("hook-auto-logger"), - } -} - -func (h *Hook) GetName() string { - return h.config.Metadata.Name -} - -func (h *Hook) GetPath() string { - return h.config.Metadata.Path -} - -func (h *Hook) GetConfig() *pkg.HookConfig { - return h.config -} - -func (h *Hook) SetMetadata(m *pkg.HookMetadata) *Hook { - h.config.Metadata = *m - - return h -} - -func (h *Hook) SetLogger(logger *log.Logger) *Hook { - h.logger = logger - - return h -} - -type HookRequest interface { - GetValues() (map[string]any, error) - GetConfigValues() (map[string]any, error) - GetBindingContexts() ([]bindingcontext.BindingContext, error) - GetDependencyContainer() pkg.DependencyContainer -} - -func (h *Hook) Execute(ctx context.Context, req HookRequest) (*HookResult, error) { - // Values are patched in-place, so an error can occur. - rawValues, err := req.GetValues() - if err != nil { - h.logger.Error("get values", slog.String("error", err.Error())) - return nil, fmt.Errorf("get values: %w", err) - } - - patchableValues, err := patchablevalues.NewPatchableValues(rawValues) - if err != nil { - h.logger.Error("new patchable values", slog.String("error", err.Error())) - return nil, fmt.Errorf("get patchable values: %w", err) - } - - rawConfigValues, err := req.GetConfigValues() - if err != nil { - h.logger.Error("get config values", slog.String("error", err.Error())) - return nil, fmt.Errorf("get config values: %w", err) - } - - patchableConfigValues, err := patchablevalues.NewPatchableValues(rawConfigValues) - if err != nil { - h.logger.Error("new patchable config values", slog.String("error", err.Error())) - return nil, fmt.Errorf("get patchable config values: %w", err) - } - - bContext, err := req.GetBindingContexts() - if err != nil { - h.logger.Warn("get binding context", slog.String("error", err.Error())) - } - - formattedSnapshots := make(objectpatch.Snapshots, len(bContext)) - for _, bc := range bContext { - for snapBindingName, snaps := range bc.Snapshots { - for _, snap := range snaps { - if snap.FilterResult != nil { - formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.FilterResult)) - - continue - } - - if snap.Object != nil { - formattedSnapshots[snapBindingName] = append(formattedSnapshots[snapBindingName], objectpatch.Snapshot(snap.Object)) - - continue - } - } - } - } - - metricsCollector := metric.NewCollector() - objectPatchCollector := objectpatch.NewObjectPatchCollector(h.logger.Named("object-patch-collector")) - - err = h.Run(ctx, &pkg.HookInput{ - Snapshots: formattedSnapshots, - Values: patchableValues, - ConfigValues: patchableConfigValues, - PatchCollector: objectPatchCollector, - MetricsCollector: metricsCollector, - DC: req.GetDependencyContainer(), - Logger: h.logger, - }) - if err != nil { - return nil, fmt.Errorf("hook reconcile func: %w", err) - } - - return &HookResult{ - Patches: map[utils.ValuesPatchType]pkg.OutputPatchableValuesCollector{ - utils.MemoryValuesPatch: patchableValues, - utils.ConfigMapPatch: patchableConfigValues, - }, - Metrics: metricsCollector, - ObjectPatcherOperations: objectPatchCollector, - }, nil -} - -// Run start ReconcileFunc -func (h *Hook) Run(ctx context.Context, input *pkg.HookInput) error { - return h.reconcileFunc(ctx, input) -} - -// HookResult returns result of a hook execution -type HookResult struct { - Patches map[utils.ValuesPatchType]pkg.OutputPatchableValuesCollector - - ObjectPatcherOperations pkg.OutputPatchCollector - Metrics pkg.OutputMetricsCollector -} diff --git a/internal/object-patch/object_patch_collector.go b/internal/object-patch/object_patch_collector.go deleted file mode 100644 index d19e96ca..00000000 --- a/internal/object-patch/object_patch_collector.go +++ /dev/null @@ -1,187 +0,0 @@ -package objectpatch - -import ( - "encoding/json" - "io" - - "github.com/deckhouse/deckhouse/pkg/log" - - "github.com/deckhouse/module-sdk/pkg" - "github.com/deckhouse/module-sdk/pkg/utils" -) - -var _ pkg.PatchCollector = (*ObjectPatchCollector)(nil) - -type ObjectPatchCollector struct { - dataStorage []Patch - logger *log.Logger -} - -func NewObjectPatchCollector(logger *log.Logger) *ObjectPatchCollector { - return &ObjectPatchCollector{ - dataStorage: make([]Patch, 0), - logger: logger, - } -} - -func (c *ObjectPatchCollector) collect(payload *Patch) { - if payload == nil { - return - } - - c.dataStorage = append(c.dataStorage, *payload) -} - -func (c *ObjectPatchCollector) Create(obj any) { - c.create(Create, obj) -} - -func (c *ObjectPatchCollector) CreateOrUpdate(obj any) { - c.create(CreateOrUpdate, obj) -} - -func (c *ObjectPatchCollector) CreateIfNotExists(obj any) { - c.create(CreateIfNotExists, obj) -} - -func (c *ObjectPatchCollector) create(operation CreateOperation, obj any) { - processed, err := utils.ToUnstructured(obj) - if err != nil { - c.logger.Error("cannot convert data to unstructured object", log.Err(err)) - - return - } - - p := &Patch{ - patchValues: map[string]any{ - "operation": operation, - "object": processed, - }, - } - - c.collect(p) -} - -func (c *ObjectPatchCollector) Delete(apiVersion string, kind string, namespace string, name string) { - c.delete(Delete, apiVersion, kind, namespace, name) -} - -func (c *ObjectPatchCollector) DeleteInBackground(apiVersion string, kind string, namespace string, name string) { - c.delete(DeleteInBackground, apiVersion, kind, namespace, name) -} - -func (c *ObjectPatchCollector) DeleteNonCascading(apiVersion string, kind string, namespace string, name string) { - c.delete(DeleteNonCascading, apiVersion, kind, namespace, name) -} - -func (c *ObjectPatchCollector) delete(operation DeleteOperation, apiVersion string, kind string, namespace string, name string) { - p := &Patch{ - patchValues: map[string]any{ - "operation": operation, - "apiVersion": apiVersion, - "kind": kind, - "name": name, - "namespace": namespace, - }, - } - - c.collect(p) -} - -func (c *ObjectPatchCollector) MergePatch(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.patch(MergePatch, patch, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) JSONPatch(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.patch(JSONPatch, patch, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) PatchWithJSON(jsonPatch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.patch(JSONPatch, jsonPatch, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) PatchWithMerge(mergePatch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.patch(MergePatch, mergePatch, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) PatchWithJQ(jqfilter string, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.filter(jqfilter, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) patch(operation PatchOperation, patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - p := &Patch{ - patchValues: map[string]any{ - "operation": operation, - "apiVersion": apiVersion, - "kind": kind, - "name": name, - "namespace": namespace, - }, - } - - switch operation { - case JQPatch: - panic("filter jq operation in patch method") - case MergePatch: - { - p.patchValues["mergePatch"] = patch - } - case JSONPatch: - { - p.patchValues["jsonPatch"] = patch - } - default: - panic("not known operation") - } - - for _, opt := range opts { - opt.Apply(p) - } - - c.collect(p) -} - -func (c *ObjectPatchCollector) JQFilter(filter string, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - c.filter(filter, apiVersion, kind, namespace, name, opts...) -} - -func (c *ObjectPatchCollector) filter(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { - p := &Patch{ - patchValues: map[string]any{ - "operation": JQPatch, - "apiVersion": apiVersion, - "kind": kind, - "name": name, - "namespace": namespace, - "jqFilter": patch, - }, - } - - for _, opt := range opts { - opt.Apply(p) - } - - c.collect(p) -} - -// Operations returns all collected operations -func (c *ObjectPatchCollector) Operations() []pkg.PatchCollectorOperation { - operations := make([]pkg.PatchCollectorOperation, 0, len(c.dataStorage)) - - for _, object := range c.dataStorage { - operations = append(operations, &object) - } - - return operations -} - -func (c *ObjectPatchCollector) WriteOutput(w io.Writer) error { - for _, object := range c.dataStorage { - err := json.NewEncoder(w).Encode(object.patchValues) - if err != nil { - return err - } - } - - return nil -} diff --git a/internal/object-patch/operation_types.go b/internal/object-patch/operation_types.go deleted file mode 100644 index 6c4e4370..00000000 --- a/internal/object-patch/operation_types.go +++ /dev/null @@ -1,28 +0,0 @@ -package objectpatch - -type CreateOperation string - -const ( - Create CreateOperation = "Create" - CreateOrUpdate CreateOperation = "CreateOrUpdate" - CreateIfNotExists CreateOperation = "CreateIfNotExists" -) - -type DeleteOperation string - -const ( - // DeletePropagationForeground - Delete DeleteOperation = "Delete" - // DeletePropagationBackground - DeleteInBackground DeleteOperation = "DeleteInBackground" - // DeletePropagationOrphan - DeleteNonCascading DeleteOperation = "DeleteNonCascading" -) - -type PatchOperation string - -const ( - MergePatch PatchOperation = "MergePatch" - JQPatch PatchOperation = "JQPatch" - JSONPatch PatchOperation = "JSONPatch" -) diff --git a/internal/object-patch/patch.go b/internal/object-patch/patch.go deleted file mode 100644 index 053cf46d..00000000 --- a/internal/object-patch/patch.go +++ /dev/null @@ -1,25 +0,0 @@ -package objectpatch - -import "github.com/deckhouse/module-sdk/pkg" - -var _ pkg.PatchCollectorOptionApplier = (*Patch)(nil) - -type Patch struct { - patchValues map[string]any -} - -func (p *Patch) Description() string { - return p.patchValues["operation"].(string) -} - -func (p *Patch) WithSubresource(subresource string) { - p.patchValues["subresource"] = subresource -} - -func (p *Patch) WithIgnoreMissingObject(ignore bool) { - p.patchValues["ignoreMissingObjects"] = ignore -} - -func (p *Patch) WithIgnoreHookError(ignore bool) { - p.patchValues["ignoreHookError"] = ignore -} diff --git a/internal/objectpatch/collector.go b/internal/objectpatch/collector.go new file mode 100644 index 00000000..21bfec64 --- /dev/null +++ b/internal/objectpatch/collector.go @@ -0,0 +1,196 @@ +package objectpatch + +import ( + "encoding/json" + "io" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +// Compile-time interface compliance check +var _ pkg.PatchCollector = (*PatchCollector)(nil) + +// PatchCollector collects Kubernetes object patch operations to be +// applied after hook execution. Supports create, delete, and various patch +// operations (JSON Patch, Merge Patch, JQ filter). +// Note: This collector is not thread-safe; do not use concurrently. +type PatchCollector struct { + dataStorage []Patch + logger *log.Logger +} + +// NewCollector creates an empty collector ready to accumulate patch operations. +func NewCollector(logger *log.Logger) *PatchCollector { + return &PatchCollector{ + dataStorage: make([]Patch, 0), + logger: logger, + } +} + +func (c *PatchCollector) collect(payload *Patch) { + if payload == nil { + return + } + + c.dataStorage = append(c.dataStorage, *payload) +} + +func (c *PatchCollector) Create(obj any) { + c.create(Create, obj) +} + +func (c *PatchCollector) CreateOrUpdate(obj any) { + c.create(CreateOrUpdate, obj) +} + +func (c *PatchCollector) CreateIfNotExists(obj any) { + c.create(CreateIfNotExists, obj) +} + +func (c *PatchCollector) create(operation CreateOperation, obj any) { + processed, err := utils.ToUnstructured(obj) + if err != nil { + c.logger.Error("cannot convert data to unstructured object", log.Err(err)) + + return + } + + c.createFromUnstructured(operation, processed) +} + +// createFromUnstructured collects a create operation with a pre-converted object. +// Used internally and by NamespacedPatchCollector for namespace injection. +func (c *PatchCollector) createFromUnstructured(operation CreateOperation, obj any) { + p := &Patch{ + patchValues: map[string]any{ + "operation": operation, + "object": obj, + }, + } + + c.collect(p) +} + +func (c *PatchCollector) Delete(apiVersion string, kind string, namespace string, name string) { + c.delete(Delete, apiVersion, kind, namespace, name) +} + +func (c *PatchCollector) DeleteInBackground(apiVersion string, kind string, namespace string, name string) { + c.delete(DeleteInBackground, apiVersion, kind, namespace, name) +} + +func (c *PatchCollector) DeleteNonCascading(apiVersion string, kind string, namespace string, name string) { + c.delete(DeleteNonCascading, apiVersion, kind, namespace, name) +} + +func (c *PatchCollector) delete(operation DeleteOperation, apiVersion string, kind string, namespace string, name string) { + p := &Patch{ + patchValues: map[string]any{ + "operation": operation, + "apiVersion": apiVersion, + "kind": kind, + "name": name, + "namespace": namespace, + }, + } + + c.collect(p) +} + +func (c *PatchCollector) MergePatch(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.patch(MergePatch, patch, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) JSONPatch(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.patch(JSONPatch, patch, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) PatchWithJSON(jsonPatch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.patch(JSONPatch, jsonPatch, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) PatchWithMerge(mergePatch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.patch(MergePatch, mergePatch, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) PatchWithJQ(jqfilter string, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.filter(jqfilter, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) patch(operation PatchOperation, patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + p := &Patch{ + patchValues: map[string]any{ + "operation": operation, + "apiVersion": apiVersion, + "kind": kind, + "name": name, + "namespace": namespace, + }, + } + + switch operation { + case JQPatch: + panic("filter jq operation in patch method") + case MergePatch: + p.patchValues["mergePatch"] = patch + case JSONPatch: + p.patchValues["jsonPatch"] = patch + default: + panic("not known operation") + } + + for _, opt := range opts { + opt.Apply(p) + } + + c.collect(p) +} + +func (c *PatchCollector) JQFilter(filter string, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + c.filter(filter, apiVersion, kind, namespace, name, opts...) +} + +func (c *PatchCollector) filter(patch any, apiVersion string, kind string, namespace string, name string, opts ...pkg.PatchCollectorOption) { + p := &Patch{ + patchValues: map[string]any{ + "operation": JQPatch, + "apiVersion": apiVersion, + "kind": kind, + "name": name, + "namespace": namespace, + "jqFilter": patch, + }, + } + + for _, opt := range opts { + opt.Apply(p) + } + + c.collect(p) +} + +// Operations returns all collected operations +func (c *PatchCollector) Operations() []pkg.PatchCollectorOperation { + operations := make([]pkg.PatchCollectorOperation, 0, len(c.dataStorage)) + + for _, object := range c.dataStorage { + operations = append(operations, &object) + } + + return operations +} + +// WriteOutput serializes all collected operations as newline-delimited JSON. +func (c *PatchCollector) WriteOutput(w io.Writer) error { + for _, object := range c.dataStorage { + err := json.NewEncoder(w).Encode(object.patchValues) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/objectpatch/namespaced.go b/internal/objectpatch/namespaced.go new file mode 100644 index 00000000..a7664716 --- /dev/null +++ b/internal/objectpatch/namespaced.go @@ -0,0 +1,100 @@ +package objectpatch + +import ( + "io" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +// Compile-time interface compliance check +var _ pkg.NamespacedPatchCollector = (*NamespacedPatchCollector)(nil) + +// NamespacedPatchCollector wraps PatchCollector to automatically inject +// namespace into all operations. Used for application hooks where the namespace +// is fixed and should not be specified by the caller. +// Note: This collector is not thread-safe; do not use concurrently. +type NamespacedPatchCollector struct { + namespace string + collector *PatchCollector +} + +func NewNamespacedCollector(namespace string, logger *log.Logger) *NamespacedPatchCollector { + return &NamespacedPatchCollector{ + namespace: namespace, + collector: NewCollector(logger), + } +} + +// Create creates the object in the cluster. +func (c *NamespacedPatchCollector) Create(obj runtime.Object) { + c.create(Create, obj) +} + +// CreateOrUpdate creates the object if it does not exist, or updates it if it does. +func (c *NamespacedPatchCollector) CreateOrUpdate(obj runtime.Object) { + c.create(CreateOrUpdate, obj) +} + +// CreateIfNotExists creates the object only if it does not already exist. +func (c *NamespacedPatchCollector) CreateIfNotExists(obj runtime.Object) { + c.create(CreateIfNotExists, obj) +} + +func (c *NamespacedPatchCollector) create(operation CreateOperation, obj runtime.Object) { + processed, err := utils.ToUnstructured(obj) + if err != nil { + c.collector.logger.Error("cannot convert data to unstructured object", log.Err(err)) + + return + } + + // Inject the fixed namespace before delegating to the underlying collector + processed.SetNamespace(c.namespace) + c.collector.createFromUnstructured(operation, processed) +} + +// Delete removes the object using foreground cascading deletion. +func (c *NamespacedPatchCollector) Delete(apiVersion, kind, name string) { + c.collector.delete(Delete, apiVersion, kind, c.namespace, name) +} + +// DeleteInBackground removes the object immediately while the garbage collector +// deletes dependents in the background. +func (c *NamespacedPatchCollector) DeleteInBackground(apiVersion, kind, name string) { + c.collector.delete(DeleteInBackground, apiVersion, kind, c.namespace, name) +} + +// DeleteNonCascading removes the object without deleting its dependents (orphans them). +func (c *NamespacedPatchCollector) DeleteNonCascading(apiVersion, kind, name string) { + c.collector.delete(DeleteNonCascading, apiVersion, kind, c.namespace, name) +} + +// PatchWithJSON applies a RFC6902 JSON Patch to the object. +func (c *NamespacedPatchCollector) PatchWithJSON(jsonPatch any, apiVersion, kind, name string, opts ...pkg.PatchCollectorOption) { + c.collector.patch(JSONPatch, jsonPatch, apiVersion, kind, c.namespace, name, opts...) +} + +// PatchWithMerge applies a RFC7396 JSON Merge Patch to the object. +func (c *NamespacedPatchCollector) PatchWithMerge(mergePatch any, apiVersion, kind, name string, opts ...pkg.PatchCollectorOption) { + c.collector.patch(MergePatch, mergePatch, apiVersion, kind, c.namespace, name, opts...) +} + +// PatchWithJQ mutates the object using a jq filter expression. +func (c *NamespacedPatchCollector) PatchWithJQ(jqfilter, apiVersion, kind, name string, opts ...pkg.PatchCollectorOption) { + c.collector.filter(jqfilter, apiVersion, kind, c.namespace, name, opts...) +} + +// Operations returns all collected operations +func (c *NamespacedPatchCollector) Operations() []pkg.PatchCollectorOperation { + return c.collector.Operations() +} + +// WriteOutput serializes all collected operations as newline-delimited JSON. +func (c *NamespacedPatchCollector) WriteOutput(w io.Writer) error { + return c.collector.WriteOutput(w) +} diff --git a/internal/objectpatch/operation.go b/internal/objectpatch/operation.go new file mode 100644 index 00000000..2b54743c --- /dev/null +++ b/internal/objectpatch/operation.go @@ -0,0 +1,32 @@ +package objectpatch + +// CreateOperation defines object creation strategies. +type CreateOperation string + +const ( + Create CreateOperation = "Create" // Always create (fails if exists) + CreateOrUpdate CreateOperation = "CreateOrUpdate" // Create or update if exists + CreateIfNotExists CreateOperation = "CreateIfNotExists" // Create only if not exists +) + +// DeleteOperation defines object deletion propagation policies. +type DeleteOperation string + +const ( + // Delete uses foreground propagation: waits for dependents to be deleted first. + Delete DeleteOperation = "Delete" + // DeleteInBackground uses background propagation: deletes object immediately, + // garbage collector handles dependents asynchronously. + DeleteInBackground DeleteOperation = "DeleteInBackground" + // DeleteNonCascading orphans dependents: removes owner references without deletion. + DeleteNonCascading DeleteOperation = "DeleteNonCascading" +) + +// PatchOperation defines how patches are applied to objects. +type PatchOperation string + +const ( + MergePatch PatchOperation = "MergePatch" // RFC7396 JSON Merge Patch + JQPatch PatchOperation = "JQPatch" // Mutate object with jq expression + JSONPatch PatchOperation = "JSONPatch" // RFC6902 JSON Patch (op/path/value) +) diff --git a/internal/objectpatch/patch.go b/internal/objectpatch/patch.go new file mode 100644 index 00000000..071bfa24 --- /dev/null +++ b/internal/objectpatch/patch.go @@ -0,0 +1,43 @@ +package objectpatch + +import ( + "fmt" + + "github.com/deckhouse/module-sdk/pkg" +) + +// Compile-time interface compliance check +var _ pkg.PatchCollectorOptionApplier = (*Patch)(nil) + +// Patch represents a single patch operation with its parameters stored as a map. +// The patchValues map is serialized to JSON when sent to the shell-operator. +type Patch struct { + patchValues map[string]any +} + +// Description returns a human-readable description of the patch operation. +// Returns "unknown" if operation type is missing or invalid. +func (p *Patch) Description() string { + op, ok := p.patchValues["operation"] + if !ok { + return "unknown" + } + + // Handle both string and typed operation enums (CreateOperation, etc.) + return fmt.Sprintf("%v", op) +} + +// WithSubresource sets the subresource to patch (e.g., "status", "scale"). +func (p *Patch) WithSubresource(subresource string) { + p.patchValues["subresource"] = subresource +} + +// WithIgnoreMissingObject prevents errors when the target object doesn't exist. +func (p *Patch) WithIgnoreMissingObject(ignore bool) { + p.patchValues["ignoreMissingObjects"] = ignore +} + +// WithIgnoreHookError continues execution even if this patch fails. +func (p *Patch) WithIgnoreHookError(ignore bool) { + p.patchValues["ignoreHookError"] = ignore +} diff --git a/internal/object-patch/object_filter.go b/internal/objectpatch/snapshot.go similarity index 100% rename from internal/object-patch/object_filter.go rename to internal/objectpatch/snapshot.go diff --git a/internal/object-patch/object_filter_test.go b/internal/objectpatch/snapshot_test.go similarity index 97% rename from internal/object-patch/object_filter_test.go rename to internal/objectpatch/snapshot_test.go index 9a7f0687..1b63b37d 100644 --- a/internal/object-patch/object_filter_test.go +++ b/internal/objectpatch/snapshot_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/assert" - objectpatch "github.com/deckhouse/module-sdk/internal/object-patch" + "github.com/deckhouse/module-sdk/internal/objectpatch" pkgobjectpatch "github.com/deckhouse/module-sdk/pkg/object-patch" ) diff --git a/internal/registry/registry.go b/internal/registry/registry.go deleted file mode 100644 index 8b5a59b1..00000000 --- a/internal/registry/registry.go +++ /dev/null @@ -1,51 +0,0 @@ -package registry - -import ( - "github.com/deckhouse/deckhouse/pkg/log" - - gohook "github.com/deckhouse/module-sdk/internal/hook" - "github.com/deckhouse/module-sdk/pkg" -) - -type HookRegistry struct { - hooks []*gohook.Hook - readinessHook *gohook.Hook - - logger *log.Logger -} - -func NewHookRegistry(logger *log.Logger) *HookRegistry { - return &HookRegistry{ - hooks: make([]*gohook.Hook, 0, 1), - logger: logger, - } -} - -// Hooks returns all hooks -func (h *HookRegistry) Hooks() []*gohook.Hook { - return h.hooks -} - -// Readiness returns the readiness hook -// It is used to check if the module is ready to serve requests -// It is not used for the readiness probe -// The readiness probe is implemented in the module itself -func (h *HookRegistry) Readiness() *gohook.Hook { - return h.readinessHook -} - -func (h *HookRegistry) Add(hooks ...*pkg.Hook) { - for _, hook := range hooks { - newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) - newHook.SetLogger(h.logger.Named(newHook.GetName())) - - h.hooks = append(h.hooks, newHook) - } -} - -func (h *HookRegistry) SetReadinessHook(hook *pkg.Hook) { - newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) - newHook.SetLogger(h.logger.Named(newHook.GetName())) - - h.readinessHook = newHook -} diff --git a/internal/transport/file/transport.go b/internal/transport/file/transport.go index 4483f175..a4344237 100644 --- a/internal/transport/file/transport.go +++ b/internal/transport/file/transport.go @@ -11,7 +11,7 @@ import ( "github.com/deckhouse/deckhouse/pkg/log" bindingcontext "github.com/deckhouse/module-sdk/internal/binding-context" - "github.com/deckhouse/module-sdk/internal/hook" + "github.com/deckhouse/module-sdk/internal/executor" "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/utils" ) @@ -210,12 +210,12 @@ type Response struct { logger *log.Logger } -func (r *Response) Send(res *hook.HookResult) error { +func (r *Response) Send(res executor.Result) error { collectors := map[string]pkg.Outputer{ - r.MetricsPath: res.Metrics, - r.KubernetesPath: res.ObjectPatcherOperations, - r.ValuesJSONPath: res.Patches[utils.MemoryValuesPatch], - r.ConfigValuesJSONPath: res.Patches[utils.ConfigMapPatch], + r.MetricsPath: res.MetricsCollector(), + r.KubernetesPath: res.ObjectPatchCollector(), + r.ValuesJSONPath: res.ValuesPatchCollector(utils.MemoryValuesPatch), + r.ConfigValuesJSONPath: res.ValuesPatchCollector(utils.ConfigMapPatch), } for path, collector := range collectors { diff --git a/pkg/dependency.go b/pkg/dependency.go index e10845bb..5086b9c2 100644 --- a/pkg/dependency.go +++ b/pkg/dependency.go @@ -28,6 +28,15 @@ type DependencyContainer interface { GetClock() clockwork.Clock } +type ApplicationDependencyContainer interface { + GetHTTPClient(options ...HTTPOption) HTTPClient + + GetRegistryClient(repo string, options ...RegistryOption) (RegistryClient, error) + MustGetRegistryClient(repo string, options ...RegistryOption) RegistryClient + + GetClock() clockwork.Clock +} + type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } diff --git a/pkg/hook.go b/pkg/hook.go index b135043a..28b0cde7 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -10,20 +10,29 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -type Hook struct { - Config *HookConfig - ReconcileFunc ReconcileFunc +const ( + EnvApplicationName = "APPLICATION_NAME" + EnvApplicationNamespace = "APPLICATION_NAMESPACE" +) + +type Input interface { + *HookInput | *ApplicationHookInput +} + +type Hook[T Input] struct { + Config *HookConfig + HookFunc HookFunc[T] } -// ReconcileFunc function which holds the main logic of the hook -type ReconcileFunc func(ctx context.Context, input *HookInput) error +// HookFunc function which holds the main logic of the hook +type HookFunc[T Input] func(ctx context.Context, input T) error type HookInput struct { Snapshots Snapshots - Values OutputPatchableValuesCollector - ConfigValues OutputPatchableValuesCollector - PatchCollector OutputPatchCollector + Values PatchableValuesCollector + ConfigValues PatchableValuesCollector + PatchCollector PatchCollector MetricsCollector MetricsCollector DC DependencyContainer @@ -31,6 +40,28 @@ type HookInput struct { Logger Logger } +type ApplicationHookInput struct { + Snapshots Snapshots + + Instance Instance + + Values PatchableValuesCollector + PatchCollector NamespacedPatchCollector + MetricsCollector MetricsCollector + + DC ApplicationDependencyContainer + + Logger Logger +} + +// Instance in application instance getter +type Instance interface { + // Name returns application instance name + Name() string + // Namespace returns application instance namespace + Namespace() string +} + type HookMetadata struct { // Hook name Name string diff --git a/pkg/object-patch/patch_options.go b/pkg/object-patch/patch_options.go index 3a72ca1a..3f91e853 100644 --- a/pkg/object-patch/patch_options.go +++ b/pkg/object-patch/patch_options.go @@ -2,24 +2,29 @@ package objectpatch import "github.com/deckhouse/module-sdk/pkg" +// PatchOption is a functional option for configuring patch operations. type PatchOption func(o pkg.PatchCollectorOptionApplier) +// Apply implements pkg.PatchCollectorOption interface. func (opt PatchOption) Apply(o pkg.PatchCollectorOptionApplier) { opt(o) } +// WithSubresource targets a specific subresource (e.g., "status", "scale"). func WithSubresource(subresource string) PatchOption { return func(o pkg.PatchCollectorOptionApplier) { o.WithSubresource(subresource) } } +// WithIgnoreMissingObject prevents errors when the target object doesn't exist. func WithIgnoreMissingObject(ignore bool) PatchOption { return func(o pkg.PatchCollectorOptionApplier) { o.WithIgnoreMissingObject(ignore) } } +// WithIgnoreHookError allows hook execution to continue even if this patch fails. func WithIgnoreHookError(ignore bool) PatchOption { return func(o pkg.PatchCollectorOptionApplier) { o.WithIgnoreHookError(ignore) diff --git a/pkg/patch.go b/pkg/patch.go index 42534e3b..eefaf441 100644 --- a/pkg/patch.go +++ b/pkg/patch.go @@ -2,22 +2,14 @@ package pkg import ( "github.com/tidwall/gjson" + "k8s.io/apimachinery/pkg/runtime" "github.com/deckhouse/module-sdk/pkg/utils" ) -type OutputPatchCollector interface { - // Deprecated: use PatchWithMerge instead - PatchCollector - Outputer -} - -type OutputPatchableValuesCollector interface { - PatchableValuesCollector +type PatchCollector interface { Outputer -} -type PatchCollector interface { // object must be Unstructured, map[string]any or runtime.Object Create(object any) // object must be Unstructured, map[string]any or runtime.Object @@ -58,6 +50,40 @@ type PatchCollector interface { Operations() []PatchCollectorOperation } +type NamespacedPatchCollector interface { + // Create creates the object in the cluster. + Create(object runtime.Object) + // CreateIfNotExists creates the object only if it does not already exist. + CreateIfNotExists(object runtime.Object) + // CreateOrUpdate creates the object if it does not exist, or updates it if it does. + CreateOrUpdate(object runtime.Object) + + // Delete removes the object using foreground cascading deletion. + // The API server adds the "foregroundDeletion" finalizer and sets deletionTimestamp. + // The object remains until the garbage collector deletes all dependents + // with ownerReference.blockOwnerDeletion=true. + Delete(apiVersion, kind, name string) + // DeleteInBackground removes the object immediately while the garbage collector + // deletes dependents in the background. + DeleteInBackground(apiVersion, kind, name string) + // DeleteNonCascading removes the object without deleting its dependents (orphans them). + DeleteNonCascading(apiVersion, kind, name string) + + // PatchWithJSON applies a RFC6902 JSON Patch to the object. + // This format requires explicit operations (add, remove, replace, etc.) with paths and values. + // See https://tools.ietf.org/html/rfc6902 for details. + PatchWithJSON(jsonPatch any, apiVersion, kind, name string, opts ...PatchCollectorOption) + // PatchWithMerge applies a RFC7396 JSON Merge Patch to the object. + // This format merges the patch directly into the object, replacing values at matching paths. + // See https://tools.ietf.org/html/rfc7396 for details. + PatchWithMerge(mergePatch any, apiVersion, kind, name string, opts ...PatchCollectorOption) + // PatchWithJQ mutates the object using a jq filter expression. + PatchWithJQ(jqfilter, apiVersion, kind, name string, opts ...PatchCollectorOption) + + // Operations returns all collected patch operations. + Operations() []PatchCollectorOperation +} + // There are 4 types of operations: // // - createOperation to create or update object via Create and Update API calls. Unstructured, map[string]any or runtime.Object is required. @@ -82,6 +108,7 @@ type PatchCollectorOptionApplier interface { } type PatchableValuesCollector interface { + Outputer ArrayCount(path string) (int, error) Exists(path string) bool Get(path string) gjson.Result diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 72562133..220bf883 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -11,87 +11,94 @@ import ( const bindingsPanicMsg = "OnStartup hook always has binding context without Kubernetes snapshots. To prevent logic errors, don't use OnStartup and Kubernetes bindings in the same Go hook configuration." -// /path/.../somemodule/hooks/001_ensure_crd/a/b/c/main.go -// $1 - Hook path for values (001_ensure_crd/a/b/c/main.go) -var hookRe = regexp.MustCompile(`([^/]*)/hooks/(.*)$`) - -var RegisterFunc = func(config *pkg.HookConfig, f pkg.ReconcileFunc) bool { - Registry().Add(&pkg.Hook{Config: config, ReconcileFunc: f}) - return true -} - -var RegisterReadinessFunc = func(config *pkg.HookConfig, f pkg.ReconcileFunc) bool { - Registry().Add(&pkg.Hook{Config: config, ReconcileFunc: f}) - return true -} - -type HookRegistry struct { - m sync.Mutex - hooks []*pkg.Hook -} - var ( instance *HookRegistry once sync.Once + + // /path/.../somemodule/hooks/001_ensure_crd/a/b/c/main.go + // $1 - Hook path for values (001_ensure_crd/a/b/c/main.go) + hookRe = regexp.MustCompile(`([^/]*)/hooks/(.*)$`) ) -// use it only in controller +type HookRegistry struct { + mtx sync.Mutex + moduleHooks []pkg.Hook[*pkg.HookInput] + applicationHooks []pkg.Hook[*pkg.ApplicationHookInput] +} + +// Registry returns singleton instance, it is used it only in controller func Registry() *HookRegistry { once.Do(func() { instance = &HookRegistry{ - hooks: make([]*pkg.Hook, 0, 1), + moduleHooks: make([]pkg.Hook[*pkg.HookInput], 0, 1), + applicationHooks: make([]pkg.Hook[*pkg.ApplicationHookInput], 0, 1), } }) + return instance } -// Hooks returns all hooks -func (h *HookRegistry) Hooks() []*pkg.Hook { - return h.hooks +func (h *HookRegistry) ModuleHooks() []pkg.Hook[*pkg.HookInput] { + return h.moduleHooks +} + +func (h *HookRegistry) ApplicationHooks() []pkg.Hook[*pkg.ApplicationHookInput] { + return h.applicationHooks } -func (h *HookRegistry) Add(hook *pkg.Hook) { - config := hook.Config - if config.OnStartup != nil && len(config.Kubernetes) > 0 { +func RegisterFunc[T pkg.Input](config *pkg.HookConfig, f pkg.HookFunc[T]) bool { + registerHook(Registry(), config, f) + return true +} + +func registerHook[T pkg.Input](r *HookRegistry, cfg *pkg.HookConfig, f pkg.HookFunc[T]) { + if cfg.OnStartup != nil && len(cfg.Kubernetes) > 0 { panic(bindingsPanicMsg) } + cfg.Metadata = extractHookMetadata() + + r.mtx.Lock() + defer r.mtx.Unlock() + + hook := pkg.Hook[T]{Config: cfg, HookFunc: f} + + switch any(hook).(type) { + case pkg.Hook[*pkg.HookInput]: + r.moduleHooks = append(r.moduleHooks, any(hook).(pkg.Hook[*pkg.HookInput])) + case pkg.Hook[*pkg.ApplicationHookInput]: + r.applicationHooks = append(r.applicationHooks, any(hook).(pkg.Hook[*pkg.ApplicationHookInput])) + default: + panic("unknown hook input type") + } +} + +func extractHookMetadata() pkg.HookMetadata { pc := make([]uintptr, 50) n := runtime.Callers(0, pc) if n == 0 { panic("runtime.Callers is empty") } - pc = pc[:n] // pass only valid pcs to runtime.CallersFrames + pc = pc[:n] frames := runtime.CallersFrames(pc) meta := pkg.HookMetadata{} - for { frame, more := frames.Next() - matches := hookRe.FindStringSubmatch(frame.File) if matches != nil { meta.Name = strings.TrimRight(matches[2], ".go") - lastSlashIdx := strings.LastIndex(matches[0], "/") - // trim with last slash - meta.Path = matches[0][:lastSlashIdx+1] } - if !more { break } } - hook.Config.Metadata = meta - - if len(hook.Config.Metadata.Name) == 0 { + if len(meta.Name) == 0 { panic("cannot extract metadata from GoHook") } - h.m.Lock() - defer h.m.Unlock() - - h.hooks = append(h.hooks, hook) + return meta } diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index b41f015a..1763a5e9 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -1,6 +1,7 @@ package registry import ( + "context" "testing" "github.com/stretchr/testify/assert" @@ -11,19 +12,15 @@ import ( func TestRegister(t *testing.T) { t.Run("Hook with OnStartup and Kubernetes bindings should panic", func(t *testing.T) { - hook := &pkg.Hook{ - Config: &pkg.HookConfig{ - OnStartup: &pkg.OrderedConfig{Order: 1}, - Kubernetes: []pkg.KubernetesConfig{ - { - Name: "test", - APIVersion: "v1", - Kind: "Pod", - // FilterFunc: nil, - }, + hook := &pkg.HookConfig{ + OnStartup: &pkg.OrderedConfig{Order: 1}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "test", + APIVersion: "v1", + Kind: "Pod", }, }, - ReconcileFunc: nil, } defer func() { @@ -31,43 +28,46 @@ func TestRegister(t *testing.T) { require.NotEmpty(t, r) assert.Equal(t, bindingsPanicMsg, r) }() - Registry().Add(hook) + + RegisterFunc(hook, func(_ context.Context, _ *pkg.HookInput) error { + return nil + }) }) t.Run("Hook with OnStartup should not panic", func(t *testing.T) { - hook := &pkg.Hook{ - Config: &pkg.HookConfig{ - OnStartup: &pkg.OrderedConfig{Order: 1}, - }, - ReconcileFunc: nil, + hook := &pkg.HookConfig{ + OnStartup: &pkg.OrderedConfig{Order: 1}, } defer func() { r := recover() assert.NotEqual(t, bindingsPanicMsg, r) }() - Registry().Add(hook) + + RegisterFunc(hook, func(_ context.Context, _ *pkg.HookInput) error { + return nil + }) }) t.Run("Hook with Kubernetes binding should not panic", func(t *testing.T) { - hook := &pkg.Hook{ - Config: &pkg.HookConfig{ - Kubernetes: []pkg.KubernetesConfig{ - { - Name: "test", - APIVersion: "v1", - Kind: "Pod", - // FilterFunc: nil, - }, + hook := &pkg.HookConfig{ + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "test", + APIVersion: "v1", + Kind: "Pod", + // FilterFunc: nil, }, }, - ReconcileFunc: nil, } defer func() { r := recover() assert.NotEqual(t, bindingsPanicMsg, r) }() - Registry().Add(hook) + + RegisterFunc(hook, func(_ context.Context, _ *pkg.HookInput) error { + return nil + }) }) } From 0030a7e41bca956d4e02d39a35998cfb88e7d416 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Sat, 17 Jan 2026 23:21:21 +0300 Subject: [PATCH 02/11] [feature] application hook Signed-off-by: Stepan Paksashvili --- pkg/patch.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/patch.go b/pkg/patch.go index eefaf441..98f8a19f 100644 --- a/pkg/patch.go +++ b/pkg/patch.go @@ -108,7 +108,6 @@ type PatchCollectorOptionApplier interface { } type PatchableValuesCollector interface { - Outputer ArrayCount(path string) (int, error) Exists(path string) bool Get(path string) gjson.Result From a275c9c1bb91b61a2f96ebd26c8321ec34b9b2d9 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili <81509933+ipaqsa@users.noreply.github.com> Date: Tue, 20 Jan 2026 15:41:46 +0300 Subject: [PATCH 03/11] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Stepan Paksashvili <81509933+ipaqsa@users.noreply.github.com> --- examples/README.md | 2 +- examples/single-file-app-example/hooks/main.go | 2 +- internal/executor/application.go | 10 ++++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/examples/README.md b/examples/README.md index 03c2e2f0..b998f8b5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,4 +14,4 @@ [Settings check example](https://github.com/deckhouse/module-sdk/tree/main/examples/settings-check) -[Application hook single file example](https://github.com/deckhouse/module-sdk/tree/main/examples/single-file-application-example) \ No newline at end of file +[Application hook single file example](https://github.com/deckhouse/module-sdk/tree/main/examples/single-file-app-example) \ No newline at end of file diff --git a/examples/single-file-app-example/hooks/main.go b/examples/single-file-app-example/hooks/main.go index 9f3d5cd7..c9994e6a 100644 --- a/examples/single-file-app-example/hooks/main.go +++ b/examples/single-file-app-example/hooks/main.go @@ -22,7 +22,7 @@ var config = &pkg.HookConfig{ Kubernetes: []pkg.KubernetesConfig{ { Name: SnapshotKey, - APIVersion: "metav1", + APIVersion: "v1", Kind: "Pod", NamespaceSelector: &pkg.NamespaceSelector{ NameSelector: &pkg.NameSelector{ diff --git a/internal/executor/application.go b/internal/executor/application.go index 1d25af34..bc99d988 100644 --- a/internal/executor/application.go +++ b/internal/executor/application.go @@ -75,13 +75,19 @@ func (e *applicationExecutor) Execute(ctx context.Context, req Request) (Result, metricsCollector := metric.NewCollector() namespacedPatchCollector := objectpatch.NewNamespacedCollector(inst.namespace, e.logger.Named("object-patch-collector")) + dc, ok := req.GetDependencyContainer().(pkg.ApplicationDependencyContainer) + if !ok { + e.logger.Error("get application dependency container", slog.String("error", "request dependency container is not an ApplicationDependencyContainer")) + return nil, fmt.Errorf("get application dependency container: incompatible dependency container type") + } + err = e.hook.HookFunc(ctx, &pkg.ApplicationHookInput{ Snapshots: formattedSnapshots, - Instance: newAppInstance(), + Instance: inst, Values: patchableValues, PatchCollector: namespacedPatchCollector, MetricsCollector: metricsCollector, - DC: req.GetDependencyContainer(), + DC: dc, Logger: e.logger, }) if err != nil { From 3b5387258dcaa5790e48b46c9499256f63cb536b Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Wed, 21 Jan 2026 17:19:16 +0300 Subject: [PATCH 04/11] fix namespace in apps Signed-off-by: Sinelnikov Michail --- internal/controller/controller.go | 47 +++++-- internal/controller/controller_test.go | 163 +++++++++++++++++++++++++ internal/executor/application.go | 4 + internal/executor/executor.go | 1 + internal/executor/module.go | 4 + 5 files changed, 211 insertions(+), 8 deletions(-) create mode 100644 internal/controller/controller_test.go diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 39ba143e..65eeb857 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -175,7 +175,11 @@ func (c *HookController) PrintHookConfigs() error { configs := make([]gohook.HookConfig, 0, 1) for _, hook := range c.registry.Executors() { - configs = append(configs, *remapHookConfigToHookConfig(hook.Config())) + hookConfig, err := remapHookConfigToHookConfig(hook.Config(), hook.IsApplicationHook()) + if err != nil { + return fmt.Errorf("failed to remap hook config for %s: %w", hook.Config().Metadata.Name, err) + } + configs = append(configs, *hookConfig) } cfg := &gohook.BatchHookConfig{ @@ -184,7 +188,11 @@ func (c *HookController) PrintHookConfigs() error { } if c.registry.Readiness() != nil { - cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().Config()) + readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config(), false) // Readiness is always a module hook + if err != nil { + return fmt.Errorf("failed to remap readiness hook config: %w", err) + } + cfg.Readiness = readinessConfig } if c.settingsCheck != nil { @@ -229,7 +237,11 @@ func (c *HookController) WriteHookConfigsInFile() error { configs := make([]gohook.HookConfig, 0, 1) for _, hook := range c.registry.Executors() { - configs = append(configs, *remapHookConfigToHookConfig(hook.Config())) + hookConfig, err := remapHookConfigToHookConfig(hook.Config(), hook.IsApplicationHook()) + if err != nil { + return fmt.Errorf("failed to remap hook config for %s: %w", hook.Config().Metadata.Name, err) + } + configs = append(configs, *hookConfig) } cfg := &gohook.BatchHookConfig{ @@ -238,7 +250,11 @@ func (c *HookController) WriteHookConfigsInFile() error { } if c.registry.Readiness() != nil { - cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().Config()) + readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config(), false) // Readiness is always a module hook + if err != nil { + return fmt.Errorf("failed to remap readiness hook config: %w", err) + } + cfg.Readiness = readinessConfig } err = json.NewEncoder(f).Encode(cfg) @@ -249,7 +265,7 @@ func (c *HookController) WriteHookConfigsInFile() error { return nil } -func remapHookConfigToHookConfig(cfg *pkg.HookConfig) *gohook.HookConfig { +func remapHookConfigToHookConfig(cfg *pkg.HookConfig, isApplicationHook bool) (*gohook.HookConfig, error) { newHookConfig := &gohook.HookConfig{ ConfigVersion: "v1", Metadata: gohook.GoHookMetadata(cfg.Metadata), @@ -290,8 +306,21 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig) *gohook.HookConfig { } } - if shcfg.NamespaceSelector != nil { - newShCfg.NamespaceSelector = &gohook.NamespaceSelector{ + var targetNamespaceSelector *gohook.NamespaceSelector + // For application hooks, automatically add namespace selector to limit resources to the app's namespace + if isApplicationHook { + appNs := os.Getenv(pkg.EnvApplicationNamespace) + if appNs == "" { + return nil, fmt.Errorf("application hook %q requires %s env var to be set", cfg.Metadata.Name, pkg.EnvApplicationNamespace) + } + + targetNamespaceSelector = &gohook.NamespaceSelector{ + NameSelector: &gohook.NameSelector{ + MatchNames: []string{appNs}, + }, + } + } else if shcfg.NamespaceSelector != nil { + targetNamespaceSelector = &gohook.NamespaceSelector{ NameSelector: &gohook.NameSelector{ MatchNames: shcfg.NamespaceSelector.NameSelector.MatchNames, }, @@ -299,6 +328,8 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig) *gohook.HookConfig { } } + newShCfg.NamespaceSelector = targetNamespaceSelector + if shcfg.FieldSelector != nil { fs := &gohook.FieldSelector{ MatchExpressions: make([]gohook.FieldSelectorRequirement, 0, len(shcfg.FieldSelector.MatchExpressions)), @@ -330,5 +361,5 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig) *gohook.HookConfig { newHookConfig.OnAfterDeleteHelm = ptr.To(cfg.OnAfterDeleteHelm.Order) } - return newHookConfig + return newHookConfig, nil } diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go new file mode 100644 index 00000000..9e8653b3 --- /dev/null +++ b/internal/controller/controller_test.go @@ -0,0 +1,163 @@ +package controller + +import ( + "context" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/deckhouse/module-sdk/internal/executor" + "github.com/deckhouse/module-sdk/pkg" +) + +type mockExecutor struct { + isAppHook bool + config *pkg.HookConfig +} + +func (m *mockExecutor) Config() *pkg.HookConfig { + return m.config +} + +func (m *mockExecutor) Execute(_ context.Context, _ executor.Request) (executor.Result, error) { + return nil, nil +} + +func (m *mockExecutor) IsApplicationHook() bool { + return m.isAppHook +} + +// Case 1: Application Hook without the specified namespace. +// Waiting: Namespace is automatically inserted from the env variable. +func Test_remapHookConfigToHookConfig_ApplicationHook_InjectsNamespace(t *testing.T) { + appName := "my-test-app" + t.Setenv(pkg.EnvApplicationNamespace, appName) + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "app-hook-simple"}, + Kubernetes: []pkg.KubernetesConfig{ + {Name: "pods", APIVersion: "v1", Kind: "Pod"}, + }, + } + + mockExec := &mockExecutor{isAppHook: true, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + require.NoError(t, err) + + require.Len(t, result.Kubernetes, 1) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector.NameSelector) + assert.Equal(t, []string{appName}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) +} + +// Case 2: Application Hook with an attempt to specify a "foreign" namespace (for example, kube-system). +// Waiting: The user config is IGNORED, the application namespace is forced. +func Test_remapHookConfigToHookConfig_ApplicationHook_OverwritesMaliciousNamespace(t *testing.T) { + appName := "my-safe-app" + maliciousNamespace := "kube-system" + t.Setenv(pkg.EnvApplicationNamespace, appName) + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "app-hook-malicious"}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "secrets", + APIVersion: "v1", + Kind: "Secret", + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{maliciousNamespace}, + }, + }, + }, + }, + } + + mockExec := &mockExecutor{isAppHook: true, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + require.NoError(t, err) + + require.Len(t, result.Kubernetes, 1) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector) + + assert.Equal(t, []string{appName}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) + assert.NotContains(t, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames, maliciousNamespace) +} + +// Case 3: Application Hook, but forgot to set the environment variable. +// Waiting: The function returns an error (Fail Fast), the config is not generated. +func Test_remapHookConfigToHookConfig_ApplicationHook_ErrorsOnMissingEnv(t *testing.T) { + os.Unsetenv(pkg.EnvApplicationNamespace) + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "app-hook-broken"}, + Kubernetes: []pkg.KubernetesConfig{ + {Name: "pods", APIVersion: "v1", Kind: "Pod"}, + }, + } + + mockExec := &mockExecutor{isAppHook: true, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + + require.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "application hook \"app-hook-broken\" requires APPLICATION_NAMESPACE env var to be set1") +} + +// Case 4: Module Hook without the namespaceSelector. +// Waiting: The NamespaceSelector remains nil (monitors the entire cluster or works by default). +func Test_remapHookConfigToHookConfig_ModuleHook_PreservesNilSelector(t *testing.T) { + t.Setenv(pkg.EnvApplicationNamespace, "some-app-ns") + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "module-hook-global"}, + Kubernetes: []pkg.KubernetesConfig{ + {Name: "nodes", APIVersion: "v1", Kind: "Node"}, + }, + } + + mockExec := &mockExecutor{isAppHook: false, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + require.NoError(t, err) + + require.Len(t, result.Kubernetes, 1) + assert.Nil(t, result.Kubernetes[0].NamespaceSelector) +} + +// Case 5: Module Hook with an explicitly specified namespace. +// Waiting: The configuration is saved as it is. +func Test_remapHookConfigToHookConfig_ModuleHook_PreservesCustomSelector(t *testing.T) { + targetNs := "kube-system" + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "module-hook-system"}, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "pods", + APIVersion: "v1", + Kind: "Pod", + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{targetNs}, + }, + }, + }, + }, + } + + mockExec := &mockExecutor{isAppHook: false, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + require.NoError(t, err) + + require.Len(t, result.Kubernetes, 1) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector) + + assert.Equal(t, []string{targetNs}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) +} diff --git a/internal/executor/application.go b/internal/executor/application.go index bc99d988..bf9d832e 100644 --- a/internal/executor/application.go +++ b/internal/executor/application.go @@ -32,6 +32,10 @@ func (e *applicationExecutor) Config() *pkg.HookConfig { return e.hook.Config } +func (e *applicationExecutor) IsApplicationHook() bool { + return true +} + func (e *applicationExecutor) Execute(ctx context.Context, req Request) (Result, error) { // Values are patched in-place, so an error can occur. rawValues, err := req.GetValues() diff --git a/internal/executor/executor.go b/internal/executor/executor.go index 7f2331e1..5218d275 100644 --- a/internal/executor/executor.go +++ b/internal/executor/executor.go @@ -11,6 +11,7 @@ import ( type Executor interface { Config() *pkg.HookConfig Execute(ctx context.Context, req Request) (Result, error) + IsApplicationHook() bool } type Request interface { diff --git a/internal/executor/module.go b/internal/executor/module.go index f54b7014..2a2a286a 100644 --- a/internal/executor/module.go +++ b/internal/executor/module.go @@ -31,6 +31,10 @@ func (e *moduleExecutor) Config() *pkg.HookConfig { return e.hook.Config } +func (e *moduleExecutor) IsApplicationHook() bool { + return false +} + func (e *moduleExecutor) Execute(ctx context.Context, req Request) (Result, error) { // Values are patched in-place, so an error can occur. rawValues, err := req.GetValues() From c772bc7ee637e0d0df4e696197ce69efc8ee9285 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 15:59:01 +0300 Subject: [PATCH 05/11] bump Signed-off-by: Sinelnikov Michail --- internal/controller/controller.go | 29 ++++++------ internal/controller/controller_test.go | 62 ++++++++------------------ pkg/hook.go | 10 +++++ pkg/registry/registry.go | 9 ++++ 4 files changed, 53 insertions(+), 57 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 65eeb857..295022de 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -175,7 +175,7 @@ func (c *HookController) PrintHookConfigs() error { configs := make([]gohook.HookConfig, 0, 1) for _, hook := range c.registry.Executors() { - hookConfig, err := remapHookConfigToHookConfig(hook.Config(), hook.IsApplicationHook()) + hookConfig, err := remapHookConfigToHookConfig(hook.Config()) if err != nil { return fmt.Errorf("failed to remap hook config for %s: %w", hook.Config().Metadata.Name, err) } @@ -188,7 +188,7 @@ func (c *HookController) PrintHookConfigs() error { } if c.registry.Readiness() != nil { - readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config(), false) // Readiness is always a module hook + readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config()) if err != nil { return fmt.Errorf("failed to remap readiness hook config: %w", err) } @@ -237,7 +237,7 @@ func (c *HookController) WriteHookConfigsInFile() error { configs := make([]gohook.HookConfig, 0, 1) for _, hook := range c.registry.Executors() { - hookConfig, err := remapHookConfigToHookConfig(hook.Config(), hook.IsApplicationHook()) + hookConfig, err := remapHookConfigToHookConfig(hook.Config()) if err != nil { return fmt.Errorf("failed to remap hook config for %s: %w", hook.Config().Metadata.Name, err) } @@ -250,7 +250,7 @@ func (c *HookController) WriteHookConfigsInFile() error { } if c.registry.Readiness() != nil { - readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config(), false) // Readiness is always a module hook + readinessConfig, err := remapHookConfigToHookConfig(c.registry.Readiness().Config()) if err != nil { return fmt.Errorf("failed to remap readiness hook config: %w", err) } @@ -265,7 +265,8 @@ func (c *HookController) WriteHookConfigsInFile() error { return nil } -func remapHookConfigToHookConfig(cfg *pkg.HookConfig, isApplicationHook bool) (*gohook.HookConfig, error) { +func remapHookConfigToHookConfig(cfg *pkg.HookConfig) (*gohook.HookConfig, error) { + isApplicationHook := cfg.HookType == pkg.HookTypeApplication newHookConfig := &gohook.HookConfig{ ConfigVersion: "v1", Metadata: gohook.GoHookMetadata(cfg.Metadata), @@ -308,24 +309,26 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig, isApplicationHook bool) (* var targetNamespaceSelector *gohook.NamespaceSelector // For application hooks, automatically add namespace selector to limit resources to the app's namespace + // For module hooks, use the configured namespace selector if present + if shcfg.NamespaceSelector != nil { + targetNamespaceSelector = &gohook.NamespaceSelector{ + NameSelector: &gohook.NameSelector{ + MatchNames: shcfg.NamespaceSelector.NameSelector.MatchNames, + }, + LabelSelector: shcfg.NamespaceSelector.LabelSelector, + } + } + // Application hooks without explicit namespace selector get app namespace if isApplicationHook { appNs := os.Getenv(pkg.EnvApplicationNamespace) if appNs == "" { return nil, fmt.Errorf("application hook %q requires %s env var to be set", cfg.Metadata.Name, pkg.EnvApplicationNamespace) } - targetNamespaceSelector = &gohook.NamespaceSelector{ NameSelector: &gohook.NameSelector{ MatchNames: []string{appNs}, }, } - } else if shcfg.NamespaceSelector != nil { - targetNamespaceSelector = &gohook.NamespaceSelector{ - NameSelector: &gohook.NameSelector{ - MatchNames: shcfg.NamespaceSelector.NameSelector.MatchNames, - }, - LabelSelector: shcfg.NamespaceSelector.LabelSelector, - } } newShCfg.NamespaceSelector = targetNamespaceSelector diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 9e8653b3..6e037503 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -18,6 +18,11 @@ type mockExecutor struct { } func (m *mockExecutor) Config() *pkg.HookConfig { + if m.isAppHook { + m.config.HookType = pkg.HookTypeApplication + } else { + m.config.HookType = pkg.HookTypeModule + } return m.config } @@ -29,14 +34,15 @@ func (m *mockExecutor) IsApplicationHook() bool { return m.isAppHook } -// Case 1: Application Hook without the specified namespace. -// Waiting: Namespace is automatically inserted from the env variable. +// Application Hook without namespace selector. +// Waiting: Namespace is automatically injected from the env variable. func Test_remapHookConfigToHookConfig_ApplicationHook_InjectsNamespace(t *testing.T) { appName := "my-test-app" t.Setenv(pkg.EnvApplicationNamespace, appName) config := &pkg.HookConfig{ Metadata: pkg.HookMetadata{Name: "app-hook-simple"}, + HookType: pkg.HookTypeApplication, Kubernetes: []pkg.KubernetesConfig{ {Name: "pods", APIVersion: "v1", Kind: "Pod"}, }, @@ -44,7 +50,7 @@ func Test_remapHookConfigToHookConfig_ApplicationHook_InjectsNamespace(t *testin mockExec := &mockExecutor{isAppHook: true, config: config} - result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + result, err := remapHookConfigToHookConfig(mockExec.Config()) require.NoError(t, err) require.Len(t, result.Kubernetes, 1) @@ -53,41 +59,6 @@ func Test_remapHookConfigToHookConfig_ApplicationHook_InjectsNamespace(t *testin assert.Equal(t, []string{appName}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) } -// Case 2: Application Hook with an attempt to specify a "foreign" namespace (for example, kube-system). -// Waiting: The user config is IGNORED, the application namespace is forced. -func Test_remapHookConfigToHookConfig_ApplicationHook_OverwritesMaliciousNamespace(t *testing.T) { - appName := "my-safe-app" - maliciousNamespace := "kube-system" - t.Setenv(pkg.EnvApplicationNamespace, appName) - - config := &pkg.HookConfig{ - Metadata: pkg.HookMetadata{Name: "app-hook-malicious"}, - Kubernetes: []pkg.KubernetesConfig{ - { - Name: "secrets", - APIVersion: "v1", - Kind: "Secret", - NamespaceSelector: &pkg.NamespaceSelector{ - NameSelector: &pkg.NameSelector{ - MatchNames: []string{maliciousNamespace}, - }, - }, - }, - }, - } - - mockExec := &mockExecutor{isAppHook: true, config: config} - - result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) - require.NoError(t, err) - - require.Len(t, result.Kubernetes, 1) - assert.NotNil(t, result.Kubernetes[0].NamespaceSelector) - - assert.Equal(t, []string{appName}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) - assert.NotContains(t, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames, maliciousNamespace) -} - // Case 3: Application Hook, but forgot to set the environment variable. // Waiting: The function returns an error (Fail Fast), the config is not generated. func Test_remapHookConfigToHookConfig_ApplicationHook_ErrorsOnMissingEnv(t *testing.T) { @@ -95,6 +66,7 @@ func Test_remapHookConfigToHookConfig_ApplicationHook_ErrorsOnMissingEnv(t *test config := &pkg.HookConfig{ Metadata: pkg.HookMetadata{Name: "app-hook-broken"}, + HookType: pkg.HookTypeApplication, Kubernetes: []pkg.KubernetesConfig{ {Name: "pods", APIVersion: "v1", Kind: "Pod"}, }, @@ -102,20 +74,21 @@ func Test_remapHookConfigToHookConfig_ApplicationHook_ErrorsOnMissingEnv(t *test mockExec := &mockExecutor{isAppHook: true, config: config} - result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + result, err := remapHookConfigToHookConfig(mockExec.Config()) require.Error(t, err) assert.Nil(t, result) - assert.Contains(t, err.Error(), "application hook \"app-hook-broken\" requires APPLICATION_NAMESPACE env var to be set1") + assert.Contains(t, err.Error(), "application hook \"app-hook-broken\" requires APPLICATION_NAMESPACE env var to be set") } -// Case 4: Module Hook without the namespaceSelector. +// Module Hook without the namespaceSelector. // Waiting: The NamespaceSelector remains nil (monitors the entire cluster or works by default). func Test_remapHookConfigToHookConfig_ModuleHook_PreservesNilSelector(t *testing.T) { t.Setenv(pkg.EnvApplicationNamespace, "some-app-ns") config := &pkg.HookConfig{ Metadata: pkg.HookMetadata{Name: "module-hook-global"}, + HookType: pkg.HookTypeModule, Kubernetes: []pkg.KubernetesConfig{ {Name: "nodes", APIVersion: "v1", Kind: "Node"}, }, @@ -123,20 +96,21 @@ func Test_remapHookConfigToHookConfig_ModuleHook_PreservesNilSelector(t *testing mockExec := &mockExecutor{isAppHook: false, config: config} - result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + result, err := remapHookConfigToHookConfig(mockExec.Config()) require.NoError(t, err) require.Len(t, result.Kubernetes, 1) assert.Nil(t, result.Kubernetes[0].NamespaceSelector) } -// Case 5: Module Hook with an explicitly specified namespace. +// Module Hook with an explicitly specified namespace. // Waiting: The configuration is saved as it is. func Test_remapHookConfigToHookConfig_ModuleHook_PreservesCustomSelector(t *testing.T) { targetNs := "kube-system" config := &pkg.HookConfig{ Metadata: pkg.HookMetadata{Name: "module-hook-system"}, + HookType: pkg.HookTypeModule, Kubernetes: []pkg.KubernetesConfig{ { Name: "pods", @@ -153,7 +127,7 @@ func Test_remapHookConfigToHookConfig_ModuleHook_PreservesCustomSelector(t *test mockExec := &mockExecutor{isAppHook: false, config: config} - result, err := remapHookConfigToHookConfig(mockExec.Config(), mockExec.IsApplicationHook()) + result, err := remapHookConfigToHookConfig(mockExec.Config()) require.NoError(t, err) require.Len(t, result.Kubernetes, 1) diff --git a/pkg/hook.go b/pkg/hook.go index 28b0cde7..af4fd6e0 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -69,6 +69,14 @@ type HookMetadata struct { Path string } +// HookType defines the type of hook +type HookType string + +const ( + HookTypeModule HookType = "module" + HookTypeApplication HookType = "application" +) + type HookConfig struct { Metadata HookMetadata Schedule []ScheduleConfig @@ -85,6 +93,8 @@ type HookConfig struct { Queue string Settings *HookConfigSettings + + HookType HookType } var ( diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 220bf883..39ceb53d 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -1,6 +1,7 @@ package registry import ( + "fmt" "regexp" "runtime" "strings" @@ -67,6 +68,14 @@ func registerHook[T pkg.Input](r *HookRegistry, cfg *pkg.HookConfig, f pkg.HookF case pkg.Hook[*pkg.HookInput]: r.moduleHooks = append(r.moduleHooks, any(hook).(pkg.Hook[*pkg.HookInput])) case pkg.Hook[*pkg.ApplicationHookInput]: + + cfg.HookType = pkg.HookTypeApplication + // Validate that application hooks don't specify namespace selectors + for _, k := range cfg.Kubernetes { + if k.NamespaceSelector != nil { + panic(fmt.Errorf("application hook cannot specify namespace selector in kubernetes config %q", k.Name)) + } + } r.applicationHooks = append(r.applicationHooks, any(hook).(pkg.Hook[*pkg.ApplicationHookInput])) default: panic("unknown hook input type") From 6fb6d24b8fb31e3409f4ecefa4b57eaca217b0f1 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 16:55:50 +0300 Subject: [PATCH 06/11] bump Signed-off-by: Sinelnikov Michail --- internal/controller/controller.go | 16 +++---- internal/controller/controller_test.go | 37 ++++++++++++++++ pkg/hook.go | 6 ++- pkg/registry/registry.go | 11 +++-- pkg/registry/registry_test.go | 60 ++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 16 deletions(-) diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 295022de..0a5dc004 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -310,15 +310,6 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig) (*gohook.HookConfig, error var targetNamespaceSelector *gohook.NamespaceSelector // For application hooks, automatically add namespace selector to limit resources to the app's namespace // For module hooks, use the configured namespace selector if present - if shcfg.NamespaceSelector != nil { - targetNamespaceSelector = &gohook.NamespaceSelector{ - NameSelector: &gohook.NameSelector{ - MatchNames: shcfg.NamespaceSelector.NameSelector.MatchNames, - }, - LabelSelector: shcfg.NamespaceSelector.LabelSelector, - } - } - // Application hooks without explicit namespace selector get app namespace if isApplicationHook { appNs := os.Getenv(pkg.EnvApplicationNamespace) if appNs == "" { @@ -329,6 +320,13 @@ func remapHookConfigToHookConfig(cfg *pkg.HookConfig) (*gohook.HookConfig, error MatchNames: []string{appNs}, }, } + } else if shcfg.NamespaceSelector != nil { + targetNamespaceSelector = &gohook.NamespaceSelector{ + NameSelector: &gohook.NameSelector{ + MatchNames: shcfg.NamespaceSelector.NameSelector.MatchNames, + }, + LabelSelector: shcfg.NamespaceSelector.LabelSelector, + } } newShCfg.NamespaceSelector = targetNamespaceSelector diff --git a/internal/controller/controller_test.go b/internal/controller/controller_test.go index 6e037503..82eb68dd 100644 --- a/internal/controller/controller_test.go +++ b/internal/controller/controller_test.go @@ -135,3 +135,40 @@ func Test_remapHookConfigToHookConfig_ModuleHook_PreservesCustomSelector(t *test assert.Equal(t, []string{targetNs}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) } + +// Application Hook with explicitly specified namespace selector. +// Waiting: The specified NamespaceSelector is ignored, and the application's namespace is used instead. +func Test_remapHookConfigToHookConfig_ApplicationHook_IgnoresCustomSelector(t *testing.T) { + appName := "my-test-app" + t.Setenv(pkg.EnvApplicationNamespace, appName) + + config := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{Name: "app-hook-with-selector"}, + HookType: pkg.HookTypeApplication, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "pods", + APIVersion: "v1", + Kind: "Pod", + // Even if NamespaceSelector is specified, it should be ignored + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{"wrong-namespace"}, + }, + }, + }, + }, + } + + mockExec := &mockExecutor{isAppHook: true, config: config} + + result, err := remapHookConfigToHookConfig(mockExec.Config()) + require.NoError(t, err) + + require.Len(t, result.Kubernetes, 1) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector) + assert.NotNil(t, result.Kubernetes[0].NamespaceSelector.NameSelector) + // Should use application namespace, not the specified one + assert.Equal(t, []string{appName}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) + assert.NotEqual(t, []string{"wrong-namespace"}, result.Kubernetes[0].NamespaceSelector.NameSelector.MatchNames) +} diff --git a/pkg/hook.go b/pkg/hook.go index af4fd6e0..03ffe6f5 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -113,13 +113,17 @@ func (cfg *HookConfig) Validate() error { } } + isApplicationHook := cfg.HookType == HookTypeApplication for _, k := range cfg.Kubernetes { if err := k.Validate(); err != nil { errs = errors.Join(errs, fmt.Errorf("kubernetes config with name '%s': %w", k.Name, err)) } + if isApplicationHook && k.NamespaceSelector != nil { + errs = errors.Join(errs, fmt.Errorf("kubernetes config with name '%s': NamespaceSelector cannot be specified for application hooks, namespace is automatically set to the application's namespace", k.Name)) + } } - return nil + return errs } type OrderedConfig struct { diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 39ceb53d..207f15b9 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -66,20 +66,19 @@ func registerHook[T pkg.Input](r *HookRegistry, cfg *pkg.HookConfig, f pkg.HookF switch any(hook).(type) { case pkg.Hook[*pkg.HookInput]: + cfg.HookType = pkg.HookTypeModule r.moduleHooks = append(r.moduleHooks, any(hook).(pkg.Hook[*pkg.HookInput])) case pkg.Hook[*pkg.ApplicationHookInput]: cfg.HookType = pkg.HookTypeApplication - // Validate that application hooks don't specify namespace selectors - for _, k := range cfg.Kubernetes { - if k.NamespaceSelector != nil { - panic(fmt.Errorf("application hook cannot specify namespace selector in kubernetes config %q", k.Name)) - } - } r.applicationHooks = append(r.applicationHooks, any(hook).(pkg.Hook[*pkg.ApplicationHookInput])) default: panic("unknown hook input type") } + + if err := cfg.Validate(); err != nil { + panic(fmt.Sprintf("hook validation failed for %q: %v", cfg.Metadata.Name, err)) + } } func extractHookMetadata() pkg.HookMetadata { diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 1763a5e9..51d34cb0 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -70,4 +70,64 @@ func TestRegister(t *testing.T) { return nil }) }) + + t.Run("Application hook with NamespaceSelector should panic", func(t *testing.T) { + hook := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{ + Name: "test-hook", + Path: "test/path", + }, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "test", + APIVersion: "v1", + Kind: "Pod", + NamespaceSelector: &pkg.NamespaceSelector{ + NameSelector: &pkg.NameSelector{ + MatchNames: []string{"some-namespace"}, + }, + }, + }, + }, + } + + defer func() { + r := recover() + require.NotEmpty(t, r) + assert.Contains(t, r.(string), "NamespaceSelector cannot be specified for application hooks") + }() + + RegisterFunc(hook, func(_ context.Context, _ *pkg.ApplicationHookInput) error { + return nil + }) + }) + + t.Run("Application hook without NamespaceSelector should not panic", func(t *testing.T) { + hook := &pkg.HookConfig{ + Metadata: pkg.HookMetadata{ + Name: "test-hook", + Path: "test/path", + }, + Kubernetes: []pkg.KubernetesConfig{ + { + Name: "test", + APIVersion: "v1", + Kind: "Pod", + }, + }, + } + + defer func() { + r := recover() + assert.NotEqual(t, bindingsPanicMsg, r) + // Should not panic with validation error + if r != nil { + assert.NotContains(t, r.(string), "NamespaceSelector cannot be specified") + } + }() + + RegisterFunc(hook, func(_ context.Context, _ *pkg.ApplicationHookInput) error { + return nil + }) + }) } From 47ea0b63f7c469f732afd256000fb2d4bbd2efa7 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 17:18:00 +0300 Subject: [PATCH 07/11] bump Signed-off-by: Sinelnikov Michail --- pkg/hook.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/hook.go b/pkg/hook.go index 03ffe6f5..4972d1b9 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -144,10 +144,6 @@ type ScheduleConfig struct { func (cfg *ScheduleConfig) Validate() error { var errs error - if !camelCaseRegexp.Match([]byte(cfg.Name)) { - errs = errors.Join(errs, errors.New("name has not letter symbols")) - } - if !cronScheduleRegex.Match([]byte(cfg.Crontab)) { errs = errors.Join(errs, errors.New("crontab is not valid")) } From 5d1ac8beceba300e14e2e86b2f48f8c3c372d8fc Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 17:35:25 +0300 Subject: [PATCH 08/11] fix tests Signed-off-by: Sinelnikov Michail --- pkg/registry/registry.go | 5 +++-- pkg/registry/registry_test.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 207f15b9..569cc6f1 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -57,7 +57,9 @@ func registerHook[T pkg.Input](r *HookRegistry, cfg *pkg.HookConfig, f pkg.HookF panic(bindingsPanicMsg) } - cfg.Metadata = extractHookMetadata() + if cfg.Metadata.Name == "" { + cfg.Metadata = extractHookMetadata() + } r.mtx.Lock() defer r.mtx.Unlock() @@ -69,7 +71,6 @@ func registerHook[T pkg.Input](r *HookRegistry, cfg *pkg.HookConfig, f pkg.HookF cfg.HookType = pkg.HookTypeModule r.moduleHooks = append(r.moduleHooks, any(hook).(pkg.Hook[*pkg.HookInput])) case pkg.Hook[*pkg.ApplicationHookInput]: - cfg.HookType = pkg.HookTypeApplication r.applicationHooks = append(r.applicationHooks, any(hook).(pkg.Hook[*pkg.ApplicationHookInput])) default: diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go index 51d34cb0..eb9965fa 100644 --- a/pkg/registry/registry_test.go +++ b/pkg/registry/registry_test.go @@ -75,7 +75,7 @@ func TestRegister(t *testing.T) { hook := &pkg.HookConfig{ Metadata: pkg.HookMetadata{ Name: "test-hook", - Path: "test/path", + Path: "test/hooks", }, Kubernetes: []pkg.KubernetesConfig{ { @@ -106,7 +106,7 @@ func TestRegister(t *testing.T) { hook := &pkg.HookConfig{ Metadata: pkg.HookMetadata{ Name: "test-hook", - Path: "test/path", + Path: "test/hooks", }, Kubernetes: []pkg.KubernetesConfig{ { From 7e6b9971fdf9d7684b29193ba76d9a0214b3d102 Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 17:49:52 +0300 Subject: [PATCH 09/11] fix Signed-off-by: Sinelnikov Michail --- pkg/hook.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pkg/hook.go b/pkg/hook.go index 4972d1b9..7c8a76bc 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -184,10 +184,6 @@ type KubernetesConfig struct { func (cfg *KubernetesConfig) Validate() error { var errs error - if !kebabCaseRegexp.Match([]byte(cfg.Name)) { - errs = errors.Join(errs, errors.New("name is not kebab case")) - } - if !camelCaseRegexp.Match([]byte(cfg.Kind)) { errs = errors.Join(errs, errors.New("kind has not letter symbols")) } From 9b4ea24ce16cfb8ee003182119980565decbcf2f Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 17:54:22 +0300 Subject: [PATCH 10/11] fix tests Signed-off-by: Sinelnikov Michail --- pkg/hook.go | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/pkg/hook.go b/pkg/hook.go index 7c8a76bc..c9380dc3 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -188,14 +188,6 @@ func (cfg *KubernetesConfig) Validate() error { errs = errors.Join(errs, errors.New("kind has not letter symbols")) } - if err := cfg.NameSelector.Validate(); err != nil { - errs = errors.Join(errs, fmt.Errorf("name selector: %w", err)) - } - - if err := cfg.NamespaceSelector.Validate(); err != nil { - errs = errors.Join(errs, fmt.Errorf("namespace selector: %w", err)) - } - return errs } @@ -203,22 +195,6 @@ type NameSelector struct { MatchNames []string } -func (cfg *NameSelector) Validate() error { - if cfg == nil { - return nil - } - - var errs error - - for _, sel := range cfg.MatchNames { - if !kebabCaseRegexp.Match([]byte(sel)) { - errs = errors.Join(errs, fmt.Errorf("selector is not kebab case '%s'", sel)) - } - } - - return errs -} - type FieldSelectorRequirement struct { Field string Operator string @@ -233,17 +209,3 @@ type NamespaceSelector struct { NameSelector *NameSelector LabelSelector *metav1.LabelSelector } - -func (cfg *NamespaceSelector) Validate() error { - if cfg == nil { - return nil - } - - var errs error - - if err := cfg.NameSelector.Validate(); err != nil { - errs = errors.Join(errs, fmt.Errorf("name selector: %w", err)) - } - - return errs -} From 1f8f44137fd873465a5d8100fd39604b14d529eb Mon Sep 17 00:00:00 2001 From: Sinelnikov Michail Date: Thu, 22 Jan 2026 18:00:06 +0300 Subject: [PATCH 11/11] fix linter Signed-off-by: Sinelnikov Michail --- pkg/hook.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/hook.go b/pkg/hook.go index c9380dc3..f963f482 100644 --- a/pkg/hook.go +++ b/pkg/hook.go @@ -98,7 +98,6 @@ type HookConfig struct { } var ( - kebabCaseRegexp = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`) camelCaseRegexp = regexp.MustCompile(`^[a-zA-Z]*$`) cronScheduleRegex = regexp.MustCompile(`^((((\d+,)+\d+|(\d+(\/|-|#)\d+)|\d+L?|\*(\/\d+)?|L(-\d+)?|\?|[A-Z]{3}(-[A-Z]{3})?) ?){5,7})|(@(annually|yearly|monthly|weekly|daily|hourly|reboot))|(@every (\d+(ns|us|µs|ms|s|m|h))+)$`) )