From 78cb1a96d75bda1ec51336cfdcecef8d6478acab Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Tue, 9 Dec 2025 12:03:08 +0300 Subject: [PATCH 01/10] values check hook Signed-off-by: Smyslov Maxim --- examples/values-check/hooks/go.mod | 92 ++++++ examples/values-check/hooks/go.sum | 255 +++++++++++++++ examples/values-check/hooks/main.go | 24 ++ internal/common-hooks/values-check/hook.go | 207 +++++++++++++ .../common-hooks/values-check/hook_test.go | 293 ++++++++++++++++++ pkg/app/app_config.go | 12 +- pkg/app/options.go | 18 ++ 7 files changed, 898 insertions(+), 3 deletions(-) create mode 100644 examples/values-check/hooks/go.mod create mode 100644 examples/values-check/hooks/go.sum create mode 100644 examples/values-check/hooks/main.go create mode 100644 internal/common-hooks/values-check/hook.go create mode 100644 internal/common-hooks/values-check/hook_test.go diff --git a/examples/values-check/hooks/go.mod b/examples/values-check/hooks/go.mod new file mode 100644 index 00000000..3d35f932 --- /dev/null +++ b/examples/values-check/hooks/go.mod @@ -0,0 +1,92 @@ +module valuescheck + +go 1.24 + +require github.com/deckhouse/module-sdk v0.0.0 + +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/deckhouse/deckhouse/pkg/log v0.0.0-20250814094423-e9f108b41a1a // 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/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/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/apimachinery 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/values-check/hooks/go.sum b/examples/values-check/hooks/go.sum new file mode 100644 index 00000000..1229124a --- /dev/null +++ b/examples/values-check/hooks/go.sum @@ -0,0 +1,255 @@ +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.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/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.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.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/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/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.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.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-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-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-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-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-20200930185726-fdedc70b468f/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-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 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/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.2/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/values-check/hooks/main.go b/examples/values-check/hooks/main.go new file mode 100644 index 00000000..e66f3b0a --- /dev/null +++ b/examples/values-check/hooks/main.go @@ -0,0 +1,24 @@ +package valuescheck + +import ( + "context" + + "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/app" +) + +const ( + SnapshotKey = "apiservers" +) + +func ValuesCheckFunc(ctx context.Context, input *pkg.HookInput) error { + return nil +} + +func main() { + valuesCheckConfig := &app.ValuesCheckConfig{ + ProbeFunc: ValuesCheckFunc, + } + + app.Run(app.WithValuesCheck(valuesCheckConfig)) +} diff --git a/internal/common-hooks/values-check/hook.go b/internal/common-hooks/values-check/hook.go new file mode 100644 index 00000000..6a63d2cf --- /dev/null +++ b/internal/common-hooks/values-check/hook.go @@ -0,0 +1,207 @@ +/* +Copyright 2022 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 valuescheck + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/deckhouse/module-sdk/pkg" + objectpatch "github.com/deckhouse/module-sdk/pkg/object-patch" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func GetModuleGVR() *schema.GroupVersionResource { + // ModuleGVR GroupVersionResource + return &schema.GroupVersionResource{ + Group: "deckhouse.io", + Version: "v1alpha1", + Resource: "modules", + } +} + +type ValuesCheckHookConfig struct { + ModuleName string + ProbeFunc func(ctx context.Context, input *pkg.HookInput) error +} + +func NewValuesCheckHookEM(cfg *ValuesCheckHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { + if cfg == nil { + panic("empty readiness config") + } + + return NewValuesCheckConfig(cfg), ModuleValuesCheck(cfg) +} + +func NewValuesCheckConfig(cfg *ValuesCheckHookConfig) *pkg.HookConfig { + return &pkg.HookConfig{ + Schedule: []pkg.ScheduleConfig{ + { + Name: cfg.ModuleName + "-moduleReadinessSchedule", + }, + }, + } +} + +const ( + conditionStatusIsReady = "IsReady" + modulePhaseReconciling = "Reconciling" + modulePhaseReady = "Ready" + modulePhaseHookError = "Error" +) + +func ModuleValuesCheck(cfg *ValuesCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { + if cfg.ModuleName == "" { + panic("empty readiness module name") + } + + if cfg.ProbeFunc == nil { + cfg.ProbeFunc = func(_ context.Context, input *pkg.HookInput) error { + input.Logger.Info("default probe function") + + return nil + } + } + + return func(ctx context.Context, input *pkg.HookInput) error { + logger := input.Logger.With(slog.String("module", cfg.ModuleName)) + logger.Info("check readiness") + + k8sClient, err := input.DC.GetK8sClient() + if err != nil { + return fmt.Errorf("get k8s client: %w", err) + } + + uModule, err := k8sClient.Dynamic().Resource(*GetModuleGVR()).Get(ctx, cfg.ModuleName, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("get module resource: %w", err) + } + + if uModule == nil { + return errors.New("unstructured object is nil") + } + + // Get conditions + uConditions, ok, err := unstructured.NestedSlice(uModule.Object, "status", "conditions") + if err != nil { + return fmt.Errorf("nested slice: failed to get status.conditions: %w", err) + } + + if !ok { + return errors.New("can't find status.conditions") + } + + if len(uConditions) == 0 { + return errors.New("status.conditions is empty") + } + + phase, ok, err := unstructured.NestedString(uModule.Object, "status", "phase") + if err != nil { + return fmt.Errorf("nested string: failed to get status.phase: %w", err) + } + + if !ok { + return errors.New("can't find status.phase") + } + + if phase != modulePhaseReconciling && phase != modulePhaseReady && phase != modulePhaseHookError { + logger.Debug("waiting for sustainable phase", slog.String("phase", phase)) + + return nil + } + + // Run probe and get status + probeStatus := string(corev1.ConditionTrue) + probeMessage := "" + probePhase := modulePhaseReady + probeReason := "" + if err := cfg.ProbeFunc(ctx, input); err != nil { + probeStatus = string(corev1.ConditionFalse) + probeMessage = err.Error() + probePhase = modulePhaseReconciling + probeReason = "ReadinessProbeFailed" + } + + // search IsReady condition + condIdx := -1 + var cond map[string]interface{} + + for idx, rawCond := range uConditions { + cond = rawCond.(map[string]interface{}) + if cond["type"].(string) == conditionStatusIsReady { + condIdx = idx + break + } + } + + if condIdx < 0 { + cond["type"] = conditionStatusIsReady + uConditions = append(uConditions, cond) + condIdx = len(uConditions) - 1 + } + + cond["lastProbeTime"] = input.DC.GetClock().Now().Format("2006-01-02T15:04:05Z") + + if cond["message"] != probeMessage || probePhase != phase { + // if probe status changed - update time + if probeStatus != cond["status"] { + cond["lastTransitionTime"] = input.DC.GetClock().Now().Format("2006-01-02T15:04:05Z") + } + + cond["status"] = probeStatus + + cond["message"] = probeMessage + if probeMessage == "" { + delete(cond, "message") + } + + cond["reason"] = probeReason + if probeReason == "" { + delete(cond, "reason") + } + + // Update module status phase + phase = probePhase + } + + uConditions[condIdx] = cond + + // creating patch + patch := map[string]any{ + "status": map[string]any{ + "conditions": uConditions, + "phase": phase, + }, + } + + input.PatchCollector.PatchWithMerge( + patch, + GetModuleGVR().GroupVersion().String(), + "Module", + "", + cfg.ModuleName, + objectpatch.WithSubresource("/status"), + ) + + return nil + } +} diff --git a/internal/common-hooks/values-check/hook_test.go b/internal/common-hooks/values-check/hook_test.go new file mode 100644 index 00000000..a6e32b9c --- /dev/null +++ b/internal/common-hooks/values-check/hook_test.go @@ -0,0 +1,293 @@ +/* +Copyright 2025 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 valuescheck_test + +import ( + "context" + "errors" + "fmt" + "testing" + "time" + + "github.com/gojuno/minimock/v3" + "github.com/jonboulle/clockwork" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/module-sdk/internal/common-hooks/readiness" + "github.com/deckhouse/module-sdk/pkg" + mock "github.com/deckhouse/module-sdk/testing/mock" +) + +func Test_ReadinessHookConfig(t *testing.T) { + t.Run("config is valid", func(t *testing.T) { + assert.NoError(t, readiness.NewReadinessConfig(&readiness.ReadinessHookConfig{}).Validate()) + }) +} + +func Test_CheckModuleReadiness(t *testing.T) { + t.Run("successful check", func(t *testing.T) { + mc := minimock.NewController(t) + defer mc.Cleanup(func() {}) + + dc := mock.NewDependencyContainerMock(mc) + patchCollector := mock.NewPatchCollectorMock(t) + + resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "IsReady", + "status": "False", + "message": "Module is not ready", + "lastTransitionTime": "2005-01-02T15:04:05Z", + }, + }, + "phase": "Reconciling", + }, + }, + } + + patch := map[string]any{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "IsReady", + "status": "True", + "lastTransitionTime": "2006-01-02T15:04:05Z", + "lastProbeTime": "2006-01-02T15:04:05Z", + }, + }, + "phase": "Ready", + }, + } + + resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) + resourceMock.GetMock. + Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). + Return(resource, nil) + + dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) + dynamicClientMock.ResourceMock. + Expect(*readiness.GetModuleGVR()). + Return(resourceMock) + + k8sClientMock := mock.NewKubernetesClientMock(mc) + k8sClientMock.DynamicMock. + Return(dynamicClientMock) + + dc.GetK8sClientMock. + Expect(). + Return(k8sClientMock, nil) + + patchCollector.PatchWithMergeMock. + Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { + assert.Equal(t, patch, mergePatch) + assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) + assert.Equal(t, kind, "Module") + assert.Equal(t, namespace, "") + assert.Equal(t, name, "stub") + }) + + clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") + assert.NoError(t, err) + + dc.GetClockMock. + Expect(). + Return(clockwork.NewFakeClockAt(clockTime)) + + input := &pkg.HookInput{ + DC: dc, + PatchCollector: patchCollector, + Logger: log.NewNop(), + } + + config := &readiness.ReadinessHookConfig{ + ModuleName: "stub", + IntervalInSeconds: 10, + ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { + return nil + }, + } + + err = readiness.CheckModuleReadiness(config)(context.Background(), input) + assert.NoError(t, err) + }) + + t.Run("k8s client error", func(t *testing.T) { + mc := minimock.NewController(t) + defer mc.Cleanup(func() {}) + + dc := mock.NewDependencyContainerMock(mc) + dc.GetK8sClientMock. + Expect(). + Return(nil, fmt.Errorf("k8s client error")) + + input := &pkg.HookInput{ + DC: dc, + Logger: log.NewNop(), + } + + config := &readiness.ReadinessHookConfig{ + ModuleName: "stub", + IntervalInSeconds: 10, + ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { + return nil + }, + } + + err := readiness.CheckModuleReadiness(config)(context.Background(), input) + assert.Error(t, err) + assert.Contains(t, err.Error(), "k8s client error") + }) + + t.Run("get resource error", func(t *testing.T) { + mc := minimock.NewController(t) + defer mc.Cleanup(func() {}) + + dc := mock.NewDependencyContainerMock(mc) + + resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) + resourceMock.GetMock. + Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). + Return(nil, fmt.Errorf("get error")) + + dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) + dynamicClientMock.ResourceMock. + Expect(*readiness.GetModuleGVR()). + Return(resourceMock) + + k8sClientMock := mock.NewKubernetesClientMock(mc) + k8sClientMock.DynamicMock. + Return(dynamicClientMock) + + dc.GetK8sClientMock. + Expect(). + Return(k8sClientMock, nil) + + input := &pkg.HookInput{ + DC: dc, + Logger: log.NewNop(), + } + + config := &readiness.ReadinessHookConfig{ + ModuleName: "stub", + IntervalInSeconds: 10, + ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { + return nil + }, + } + + err := readiness.CheckModuleReadiness(config)(context.Background(), input) + assert.Error(t, err) + assert.Contains(t, err.Error(), "get error") + }) + + t.Run("readiness error", func(t *testing.T) { + mc := minimock.NewController(t) + defer mc.Cleanup(func() {}) + + dc := mock.NewDependencyContainerMock(mc) + patchCollector := mock.NewPatchCollectorMock(t) + + resource := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "IsReady", + "status": "True", + "lastTransitionTime": "2005-01-02T15:04:05Z", + }, + }, + "phase": "Reconciling", + }, + }, + } + + patch := map[string]any{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "IsReady", + "status": "False", + "message": "readiness error", + "reason": "ReadinessProbeFailed", + "lastTransitionTime": "2006-01-02T15:04:05Z", + "lastProbeTime": "2006-01-02T15:04:05Z", + }, + }, + "phase": "Reconciling", + }, + } + + resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) + resourceMock.GetMock. + Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). + Return(resource, nil) + + dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) + dynamicClientMock.ResourceMock. + Expect(*readiness.GetModuleGVR()). + Return(resourceMock) + + k8sClientMock := mock.NewKubernetesClientMock(mc) + k8sClientMock.DynamicMock. + Return(dynamicClientMock) + + dc.GetK8sClientMock. + Expect(). + Return(k8sClientMock, nil) + + patchCollector.PatchWithMergeMock. + Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { + assert.Equal(t, patch, mergePatch) + assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) + assert.Equal(t, kind, "Module") + assert.Equal(t, namespace, "") + assert.Equal(t, name, "stub") + }) + + clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") + assert.NoError(t, err) + + dc.GetClockMock. + Expect(). + Return(clockwork.NewFakeClockAt(clockTime)) + + input := &pkg.HookInput{ + DC: dc, + PatchCollector: patchCollector, + Logger: log.NewNop(), + } + + config := &readiness.ReadinessHookConfig{ + ModuleName: "stub", + IntervalInSeconds: 10, + ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { + return errors.New("readiness error") + }, + } + + err = readiness.CheckModuleReadiness(config)(context.Background(), input) + assert.NoError(t, err) + }) +} diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go index 1e445c0d..97d0e639 100644 --- a/pkg/app/app_config.go +++ b/pkg/app/app_config.go @@ -40,10 +40,16 @@ type readinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } +type valuesCheckConfig struct { + ModuleName string + ProbeFunc func(ctx context.Context, input *pkg.HookInput) error +} + type config struct { - ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` - HookConfig *hookConfig - ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` + ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` + HookConfig *hookConfig + ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` + ValuesCheckConfig *valuesCheckConfig `envPrefix:"VALUES_CHECK_"` LogLevelRaw string `env:"LOG_LEVEL" envDefault:"FATAL"` LogLevel log.Level `env:"-"` diff --git a/pkg/app/options.go b/pkg/app/options.go index f557242b..db4ea055 100644 --- a/pkg/app/options.go +++ b/pkg/app/options.go @@ -32,3 +32,21 @@ type ReadinessConfig struct { IntervalInSeconds uint8 ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } + +func WithValuesCheck(cfg *ValuesCheckConfig) RunConfigOption { + if cfg == nil { + return func(c *config) { + c.ValuesCheckConfig = nil + } + } + + return func(c *config) { + c.ValuesCheckConfig = &valuesCheckConfig{ + ProbeFunc: cfg.ProbeFunc, + } + } +} + +type ValuesCheckConfig struct { + ProbeFunc func(ctx context.Context, input *pkg.HookInput) error +} From 12e9c8db509560edbe5c6e2837481c7951c2433a Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Tue, 9 Dec 2025 14:34:04 +0300 Subject: [PATCH 02/10] full path as in readiness Signed-off-by: Smyslov Maxim --- examples/values-check/hooks/main.go | 12 +- .../common-hooks/values-check/hook_test.go | 293 ------------------ internal/controller/config.go | 7 +- internal/controller/controller.go | 56 ++++ internal/registry/registry.go | 16 +- pkg/app/app_config.go | 20 +- pkg/app/options.go | 16 +- pkg/app/root.go | 17 + .../settings-check}/hook.go | 12 +- pkg/settings-check/hook_test.go | 293 ++++++++++++++++++ 10 files changed, 414 insertions(+), 328 deletions(-) delete mode 100644 internal/common-hooks/values-check/hook_test.go rename {internal/common-hooks/values-check => pkg/settings-check}/hook.go (92%) create mode 100644 pkg/settings-check/hook_test.go diff --git a/examples/values-check/hooks/main.go b/examples/values-check/hooks/main.go index e66f3b0a..c2652b50 100644 --- a/examples/values-check/hooks/main.go +++ b/examples/values-check/hooks/main.go @@ -1,7 +1,8 @@ -package valuescheck +package main import ( "context" + "fmt" "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/app" @@ -11,14 +12,11 @@ const ( SnapshotKey = "apiservers" ) -func ValuesCheckFunc(ctx context.Context, input *pkg.HookInput) error { +func SettingsCheckFunc(ctx context.Context, input *pkg.HookInput) error { + fmt.Println("settings check") return nil } func main() { - valuesCheckConfig := &app.ValuesCheckConfig{ - ProbeFunc: ValuesCheckFunc, - } - - app.Run(app.WithValuesCheck(valuesCheckConfig)) + app.Run(app.WithSettingsCheck(SettingsCheckFunc)) } diff --git a/internal/common-hooks/values-check/hook_test.go b/internal/common-hooks/values-check/hook_test.go deleted file mode 100644 index a6e32b9c..00000000 --- a/internal/common-hooks/values-check/hook_test.go +++ /dev/null @@ -1,293 +0,0 @@ -/* -Copyright 2025 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 valuescheck_test - -import ( - "context" - "errors" - "fmt" - "testing" - "time" - - "github.com/gojuno/minimock/v3" - "github.com/jonboulle/clockwork" - "github.com/stretchr/testify/assert" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - - "github.com/deckhouse/deckhouse/pkg/log" - - "github.com/deckhouse/module-sdk/internal/common-hooks/readiness" - "github.com/deckhouse/module-sdk/pkg" - mock "github.com/deckhouse/module-sdk/testing/mock" -) - -func Test_ReadinessHookConfig(t *testing.T) { - t.Run("config is valid", func(t *testing.T) { - assert.NoError(t, readiness.NewReadinessConfig(&readiness.ReadinessHookConfig{}).Validate()) - }) -} - -func Test_CheckModuleReadiness(t *testing.T) { - t.Run("successful check", func(t *testing.T) { - mc := minimock.NewController(t) - defer mc.Cleanup(func() {}) - - dc := mock.NewDependencyContainerMock(mc) - patchCollector := mock.NewPatchCollectorMock(t) - - resource := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "type": "IsReady", - "status": "False", - "message": "Module is not ready", - "lastTransitionTime": "2005-01-02T15:04:05Z", - }, - }, - "phase": "Reconciling", - }, - }, - } - - patch := map[string]any{ - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "type": "IsReady", - "status": "True", - "lastTransitionTime": "2006-01-02T15:04:05Z", - "lastProbeTime": "2006-01-02T15:04:05Z", - }, - }, - "phase": "Ready", - }, - } - - resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) - resourceMock.GetMock. - Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). - Return(resource, nil) - - dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) - dynamicClientMock.ResourceMock. - Expect(*readiness.GetModuleGVR()). - Return(resourceMock) - - k8sClientMock := mock.NewKubernetesClientMock(mc) - k8sClientMock.DynamicMock. - Return(dynamicClientMock) - - dc.GetK8sClientMock. - Expect(). - Return(k8sClientMock, nil) - - patchCollector.PatchWithMergeMock. - Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { - assert.Equal(t, patch, mergePatch) - assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) - assert.Equal(t, kind, "Module") - assert.Equal(t, namespace, "") - assert.Equal(t, name, "stub") - }) - - clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") - assert.NoError(t, err) - - dc.GetClockMock. - Expect(). - Return(clockwork.NewFakeClockAt(clockTime)) - - input := &pkg.HookInput{ - DC: dc, - PatchCollector: patchCollector, - Logger: log.NewNop(), - } - - config := &readiness.ReadinessHookConfig{ - ModuleName: "stub", - IntervalInSeconds: 10, - ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { - return nil - }, - } - - err = readiness.CheckModuleReadiness(config)(context.Background(), input) - assert.NoError(t, err) - }) - - t.Run("k8s client error", func(t *testing.T) { - mc := minimock.NewController(t) - defer mc.Cleanup(func() {}) - - dc := mock.NewDependencyContainerMock(mc) - dc.GetK8sClientMock. - Expect(). - Return(nil, fmt.Errorf("k8s client error")) - - input := &pkg.HookInput{ - DC: dc, - Logger: log.NewNop(), - } - - config := &readiness.ReadinessHookConfig{ - ModuleName: "stub", - IntervalInSeconds: 10, - ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { - return nil - }, - } - - err := readiness.CheckModuleReadiness(config)(context.Background(), input) - assert.Error(t, err) - assert.Contains(t, err.Error(), "k8s client error") - }) - - t.Run("get resource error", func(t *testing.T) { - mc := minimock.NewController(t) - defer mc.Cleanup(func() {}) - - dc := mock.NewDependencyContainerMock(mc) - - resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) - resourceMock.GetMock. - Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). - Return(nil, fmt.Errorf("get error")) - - dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) - dynamicClientMock.ResourceMock. - Expect(*readiness.GetModuleGVR()). - Return(resourceMock) - - k8sClientMock := mock.NewKubernetesClientMock(mc) - k8sClientMock.DynamicMock. - Return(dynamicClientMock) - - dc.GetK8sClientMock. - Expect(). - Return(k8sClientMock, nil) - - input := &pkg.HookInput{ - DC: dc, - Logger: log.NewNop(), - } - - config := &readiness.ReadinessHookConfig{ - ModuleName: "stub", - IntervalInSeconds: 10, - ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { - return nil - }, - } - - err := readiness.CheckModuleReadiness(config)(context.Background(), input) - assert.Error(t, err) - assert.Contains(t, err.Error(), "get error") - }) - - t.Run("readiness error", func(t *testing.T) { - mc := minimock.NewController(t) - defer mc.Cleanup(func() {}) - - dc := mock.NewDependencyContainerMock(mc) - patchCollector := mock.NewPatchCollectorMock(t) - - resource := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "type": "IsReady", - "status": "True", - "lastTransitionTime": "2005-01-02T15:04:05Z", - }, - }, - "phase": "Reconciling", - }, - }, - } - - patch := map[string]any{ - "status": map[string]interface{}{ - "conditions": []interface{}{ - map[string]interface{}{ - "type": "IsReady", - "status": "False", - "message": "readiness error", - "reason": "ReadinessProbeFailed", - "lastTransitionTime": "2006-01-02T15:04:05Z", - "lastProbeTime": "2006-01-02T15:04:05Z", - }, - }, - "phase": "Reconciling", - }, - } - - resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) - resourceMock.GetMock. - Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). - Return(resource, nil) - - dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) - dynamicClientMock.ResourceMock. - Expect(*readiness.GetModuleGVR()). - Return(resourceMock) - - k8sClientMock := mock.NewKubernetesClientMock(mc) - k8sClientMock.DynamicMock. - Return(dynamicClientMock) - - dc.GetK8sClientMock. - Expect(). - Return(k8sClientMock, nil) - - patchCollector.PatchWithMergeMock. - Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { - assert.Equal(t, patch, mergePatch) - assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) - assert.Equal(t, kind, "Module") - assert.Equal(t, namespace, "") - assert.Equal(t, name, "stub") - }) - - clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") - assert.NoError(t, err) - - dc.GetClockMock. - Expect(). - Return(clockwork.NewFakeClockAt(clockTime)) - - input := &pkg.HookInput{ - DC: dc, - PatchCollector: patchCollector, - Logger: log.NewNop(), - } - - config := &readiness.ReadinessHookConfig{ - ModuleName: "stub", - IntervalInSeconds: 10, - ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { - return errors.New("readiness error") - }, - } - - err = readiness.CheckModuleReadiness(config)(context.Background(), input) - assert.NoError(t, err) - }) -} diff --git a/internal/controller/config.go b/internal/controller/config.go index 6698fa7a..152a977a 100644 --- a/internal/controller/config.go +++ b/internal/controller/config.go @@ -32,9 +32,10 @@ type ReadinessConfig struct { } type Config struct { - ModuleName string - HookConfig *HookConfig - ReadinessConfig *ReadinessConfig + ModuleName string + HookConfig *HookConfig + ReadinessConfig *ReadinessConfig + SettingsCheckConfig *SettingsCheckConfig LogLevelRaw string LogLevel log.Level diff --git a/internal/controller/controller.go b/internal/controller/controller.go index d802dd7d..b7521b6c 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -18,6 +18,7 @@ import ( "github.com/deckhouse/module-sdk/pkg/dependency" gohook "github.com/deckhouse/module-sdk/pkg/hook" outerRegistry "github.com/deckhouse/module-sdk/pkg/registry" + settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" "github.com/deckhouse/module-sdk/pkg/utils/ptr" ) @@ -45,6 +46,10 @@ func NewHookController(cfg *Config, logger *log.Logger) *HookController { addReadinessHook(reg, cfg.ReadinessConfig) } + if cfg.SettingsCheckConfig != nil { + addSettingsCheckHook(reg, cfg.SettingsCheckConfig) + } + return &HookController{ registry: reg, dc: dependency.NewDependencyContainer(), @@ -147,6 +152,57 @@ func (c *HookController) RunReadiness(ctx context.Context) error { return nil } +var ErrSettingsCheckHookDoesNotExists = errors.New("settings check hook does not exists") + +func (c *HookController) RunSettingsCheck(ctx context.Context) error { + hook := c.registry.SettingsCheck() + + if hook == nil { + return ErrReadinessHookDoesNotExists + } + + transport := file.NewTransport(c.fConfig, hook.GetName(), c.dc, c.logger.Named("file-transport")) + + hookRes, err := hook.Execute(ctx, transport.NewRequest()) + if err != nil { + outputError := &gohook.Error{Message: "execute: " + err.Error()} + + buf := bytes.NewBuffer([]byte{}) + err := json.NewEncoder(buf).Encode(outputError) + if err != nil { + return fmt.Errorf("encode error: %w", err) + } + + fmt.Fprintln(os.Stderr, buf.String()) + os.Exit(1) + } + + err = transport.NewResponse().Send(hookRes) + if err != nil { + return fmt.Errorf("send: %w", err) + } + + return nil +} + +type SettingsCheckConfig struct { + ModuleName string + ProbeFunc func(ctx context.Context, input *pkg.HookInput) error +} + +func addSettingsCheckHook(reg *registry.HookRegistry, cfg *SettingsCheckConfig) { + settingsCheckConfig := &settingscheck.SettingsCheckHookConfig{ + ModuleName: cfg.ModuleName, + ProbeFunc: cfg.ProbeFunc, + } + + config, f := settingscheck.NewSettingsCheckHookEM(settingsCheckConfig) + config.Metadata.Name = "settings-check" + config.Metadata.Path = "common-hooks/settings-check" + + reg.SetSettingsCheckHook(&pkg.Hook{Config: config, ReconcileFunc: f}) +} + var ErrNoHooksRegistered = errors.New("no hooks registered") func (c *HookController) PrintHookConfigs() error { diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 8b5a59b1..893ee405 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -8,8 +8,9 @@ import ( ) type HookRegistry struct { - hooks []*gohook.Hook - readinessHook *gohook.Hook + hooks []*gohook.Hook + readinessHook *gohook.Hook + settingsCheckHook *gohook.Hook logger *log.Logger } @@ -34,6 +35,10 @@ func (h *HookRegistry) Readiness() *gohook.Hook { return h.readinessHook } +func (h *HookRegistry) SettingsCheck() *gohook.Hook { + return h.settingsCheckHook +} + func (h *HookRegistry) Add(hooks ...*pkg.Hook) { for _, hook := range hooks { newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) @@ -49,3 +54,10 @@ func (h *HookRegistry) SetReadinessHook(hook *pkg.Hook) { h.readinessHook = newHook } + +func (h *HookRegistry) SetSettingsCheckHook(hook *pkg.Hook) { + newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) + newHook.SetLogger(h.logger.Named(newHook.GetName())) + + h.settingsCheckHook = newHook +} diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go index 97d0e639..974bac9f 100644 --- a/pkg/app/app_config.go +++ b/pkg/app/app_config.go @@ -40,16 +40,15 @@ type readinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -type valuesCheckConfig struct { - ModuleName string - ProbeFunc func(ctx context.Context, input *pkg.HookInput) error +type settingsCheckConfig struct { + ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } type config struct { - ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` - HookConfig *hookConfig - ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` - ValuesCheckConfig *valuesCheckConfig `envPrefix:"VALUES_CHECK_"` + ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` + HookConfig *hookConfig + ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` + SettingsCheckConfig *settingsCheckConfig LogLevelRaw string `env:"LOG_LEVEL" envDefault:"FATAL"` LogLevel log.Level `env:"-"` @@ -103,5 +102,12 @@ func remapConfigToControllerConfig(input *config) *controller.Config { } } + if input.SettingsCheckConfig != nil { + cfg.SettingsCheckConfig = &controller.SettingsCheckConfig{ + ModuleName: input.ModuleName, + ProbeFunc: input.SettingsCheckConfig.ProbeFunc, + } + } + return cfg } diff --git a/pkg/app/options.go b/pkg/app/options.go index db4ea055..2c7cc01e 100644 --- a/pkg/app/options.go +++ b/pkg/app/options.go @@ -33,20 +33,16 @@ type ReadinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -func WithValuesCheck(cfg *ValuesCheckConfig) RunConfigOption { - if cfg == nil { - return func(c *config) { - c.ValuesCheckConfig = nil - } +func WithSettingsCheck(probeFunc settingsProbeFunc) RunConfigOption { + if probeFunc == nil { + return func(c *config) {} } return func(c *config) { - c.ValuesCheckConfig = &valuesCheckConfig{ - ProbeFunc: cfg.ProbeFunc, + c.SettingsCheckConfig = &settingsCheckConfig{ + ProbeFunc: probeFunc, } } } -type ValuesCheckConfig struct { - ProbeFunc func(ctx context.Context, input *pkg.HookInput) error -} +type settingsProbeFunc func(ctx context.Context, input *pkg.HookInput) error diff --git a/pkg/app/root.go b/pkg/app/root.go index cc342716..318a0b41 100644 --- a/pkg/app/root.go +++ b/pkg/app/root.go @@ -78,6 +78,22 @@ func (c *cmd) hooksCmd() *cobra.Command { }, }) + settingsCheckCmd := &cobra.Command{ + Use: "check", + Short: "Check settings", + Long: `Check settings of the module`, + Hidden: true, + Run: func(cmd *cobra.Command, _ []string) { + ctx := cmd.Context() + + err := c.controller.RunSettingsCheck(ctx) + if err != nil { + c.logger.Warn("settings check hook shutdown", "error", err) + os.Exit(1) + } + }, + } + configCmd := &cobra.Command{ Use: "config", Short: "Print hooks configs", @@ -92,6 +108,7 @@ func (c *cmd) hooksCmd() *cobra.Command { return nil }, } + configCmd.AddCommand(settingsCheckCmd) hooksCmd.AddCommand(configCmd) dumpCmd := &cobra.Command{ diff --git a/internal/common-hooks/values-check/hook.go b/pkg/settings-check/hook.go similarity index 92% rename from internal/common-hooks/values-check/hook.go rename to pkg/settings-check/hook.go index 6a63d2cf..8b61b1e1 100644 --- a/internal/common-hooks/values-check/hook.go +++ b/pkg/settings-check/hook.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package valuescheck +package settingscheck import ( "context" @@ -39,20 +39,20 @@ func GetModuleGVR() *schema.GroupVersionResource { } } -type ValuesCheckHookConfig struct { +type SettingsCheckHookConfig struct { ModuleName string ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -func NewValuesCheckHookEM(cfg *ValuesCheckHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { +func NewSettingsCheckHookEM(cfg *SettingsCheckHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { if cfg == nil { panic("empty readiness config") } - return NewValuesCheckConfig(cfg), ModuleValuesCheck(cfg) + return NewSettingsCheckConfig(cfg), ModuleSettingsCheck(cfg) } -func NewValuesCheckConfig(cfg *ValuesCheckHookConfig) *pkg.HookConfig { +func NewSettingsCheckConfig(cfg *SettingsCheckHookConfig) *pkg.HookConfig { return &pkg.HookConfig{ Schedule: []pkg.ScheduleConfig{ { @@ -69,7 +69,7 @@ const ( modulePhaseHookError = "Error" ) -func ModuleValuesCheck(cfg *ValuesCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { +func ModuleSettingsCheck(cfg *SettingsCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { if cfg.ModuleName == "" { panic("empty readiness module name") } diff --git a/pkg/settings-check/hook_test.go b/pkg/settings-check/hook_test.go new file mode 100644 index 00000000..5d66af33 --- /dev/null +++ b/pkg/settings-check/hook_test.go @@ -0,0 +1,293 @@ +// /* +// Copyright 2025 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 settingscheck_test + +// import ( +// "context" +// "errors" +// "fmt" +// "testing" +// "time" + +// "github.com/gojuno/minimock/v3" +// "github.com/jonboulle/clockwork" +// "github.com/stretchr/testify/assert" +// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + +// "github.com/deckhouse/deckhouse/pkg/log" + +// "github.com/deckhouse/module-sdk/internal/common-hooks/readiness" +// "github.com/deckhouse/module-sdk/pkg" +// mock "github.com/deckhouse/module-sdk/testing/mock" +// ) + +// func Test_ReadinessHookConfig(t *testing.T) { +// t.Run("config is valid", func(t *testing.T) { +// assert.NoError(t, readiness.NewReadinessConfig(&readiness.ReadinessHookConfig{}).Validate()) +// }) +// } + +// func Test_CheckModuleReadiness(t *testing.T) { +// t.Run("successful check", func(t *testing.T) { +// mc := minimock.NewController(t) +// defer mc.Cleanup(func() {}) + +// dc := mock.NewDependencyContainerMock(mc) +// patchCollector := mock.NewPatchCollectorMock(t) + +// resource := &unstructured.Unstructured{ +// Object: map[string]interface{}{ +// "status": map[string]interface{}{ +// "conditions": []interface{}{ +// map[string]interface{}{ +// "type": "IsReady", +// "status": "False", +// "message": "Module is not ready", +// "lastTransitionTime": "2005-01-02T15:04:05Z", +// }, +// }, +// "phase": "Reconciling", +// }, +// }, +// } + +// patch := map[string]any{ +// "status": map[string]interface{}{ +// "conditions": []interface{}{ +// map[string]interface{}{ +// "type": "IsReady", +// "status": "True", +// "lastTransitionTime": "2006-01-02T15:04:05Z", +// "lastProbeTime": "2006-01-02T15:04:05Z", +// }, +// }, +// "phase": "Ready", +// }, +// } + +// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) +// resourceMock.GetMock. +// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). +// Return(resource, nil) + +// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) +// dynamicClientMock.ResourceMock. +// Expect(*readiness.GetModuleGVR()). +// Return(resourceMock) + +// k8sClientMock := mock.NewKubernetesClientMock(mc) +// k8sClientMock.DynamicMock. +// Return(dynamicClientMock) + +// dc.GetK8sClientMock. +// Expect(). +// Return(k8sClientMock, nil) + +// patchCollector.PatchWithMergeMock. +// Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { +// assert.Equal(t, patch, mergePatch) +// assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) +// assert.Equal(t, kind, "Module") +// assert.Equal(t, namespace, "") +// assert.Equal(t, name, "stub") +// }) + +// clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") +// assert.NoError(t, err) + +// dc.GetClockMock. +// Expect(). +// Return(clockwork.NewFakeClockAt(clockTime)) + +// input := &pkg.HookInput{ +// DC: dc, +// PatchCollector: patchCollector, +// Logger: log.NewNop(), +// } + +// config := &readiness.ReadinessHookConfig{ +// ModuleName: "stub", +// IntervalInSeconds: 10, +// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { +// return nil +// }, +// } + +// err = readiness.CheckModuleReadiness(config)(context.Background(), input) +// assert.NoError(t, err) +// }) + +// t.Run("k8s client error", func(t *testing.T) { +// mc := minimock.NewController(t) +// defer mc.Cleanup(func() {}) + +// dc := mock.NewDependencyContainerMock(mc) +// dc.GetK8sClientMock. +// Expect(). +// Return(nil, fmt.Errorf("k8s client error")) + +// input := &pkg.HookInput{ +// DC: dc, +// Logger: log.NewNop(), +// } + +// config := &readiness.ReadinessHookConfig{ +// ModuleName: "stub", +// IntervalInSeconds: 10, +// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { +// return nil +// }, +// } + +// err := readiness.CheckModuleReadiness(config)(context.Background(), input) +// assert.Error(t, err) +// assert.Contains(t, err.Error(), "k8s client error") +// }) + +// t.Run("get resource error", func(t *testing.T) { +// mc := minimock.NewController(t) +// defer mc.Cleanup(func() {}) + +// dc := mock.NewDependencyContainerMock(mc) + +// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) +// resourceMock.GetMock. +// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). +// Return(nil, fmt.Errorf("get error")) + +// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) +// dynamicClientMock.ResourceMock. +// Expect(*readiness.GetModuleGVR()). +// Return(resourceMock) + +// k8sClientMock := mock.NewKubernetesClientMock(mc) +// k8sClientMock.DynamicMock. +// Return(dynamicClientMock) + +// dc.GetK8sClientMock. +// Expect(). +// Return(k8sClientMock, nil) + +// input := &pkg.HookInput{ +// DC: dc, +// Logger: log.NewNop(), +// } + +// config := &readiness.ReadinessHookConfig{ +// ModuleName: "stub", +// IntervalInSeconds: 10, +// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { +// return nil +// }, +// } + +// err := readiness.CheckModuleReadiness(config)(context.Background(), input) +// assert.Error(t, err) +// assert.Contains(t, err.Error(), "get error") +// }) + +// t.Run("readiness error", func(t *testing.T) { +// mc := minimock.NewController(t) +// defer mc.Cleanup(func() {}) + +// dc := mock.NewDependencyContainerMock(mc) +// patchCollector := mock.NewPatchCollectorMock(t) + +// resource := &unstructured.Unstructured{ +// Object: map[string]interface{}{ +// "status": map[string]interface{}{ +// "conditions": []interface{}{ +// map[string]interface{}{ +// "type": "IsReady", +// "status": "True", +// "lastTransitionTime": "2005-01-02T15:04:05Z", +// }, +// }, +// "phase": "Reconciling", +// }, +// }, +// } + +// patch := map[string]any{ +// "status": map[string]interface{}{ +// "conditions": []interface{}{ +// map[string]interface{}{ +// "type": "IsReady", +// "status": "False", +// "message": "readiness error", +// "reason": "ReadinessProbeFailed", +// "lastTransitionTime": "2006-01-02T15:04:05Z", +// "lastProbeTime": "2006-01-02T15:04:05Z", +// }, +// }, +// "phase": "Reconciling", +// }, +// } + +// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) +// resourceMock.GetMock. +// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). +// Return(resource, nil) + +// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) +// dynamicClientMock.ResourceMock. +// Expect(*readiness.GetModuleGVR()). +// Return(resourceMock) + +// k8sClientMock := mock.NewKubernetesClientMock(mc) +// k8sClientMock.DynamicMock. +// Return(dynamicClientMock) + +// dc.GetK8sClientMock. +// Expect(). +// Return(k8sClientMock, nil) + +// patchCollector.PatchWithMergeMock. +// Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { +// assert.Equal(t, patch, mergePatch) +// assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) +// assert.Equal(t, kind, "Module") +// assert.Equal(t, namespace, "") +// assert.Equal(t, name, "stub") +// }) + +// clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") +// assert.NoError(t, err) + +// dc.GetClockMock. +// Expect(). +// Return(clockwork.NewFakeClockAt(clockTime)) + +// input := &pkg.HookInput{ +// DC: dc, +// PatchCollector: patchCollector, +// Logger: log.NewNop(), +// } + +// config := &readiness.ReadinessHookConfig{ +// ModuleName: "stub", +// IntervalInSeconds: 10, +// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { +// return errors.New("readiness error") +// }, +// } + +// err = readiness.CheckModuleReadiness(config)(context.Background(), input) +// assert.NoError(t, err) +// }) +// } From 4e8d6102115c8a2565ade4a53e2f085349a6fbc0 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Tue, 9 Dec 2025 15:24:16 +0300 Subject: [PATCH 03/10] working prototype Signed-off-by: Smyslov Maxim --- examples/values-check/hooks/main.go | 19 +++- internal/controller/controller.go | 4 +- pkg/app/app_config.go | 3 +- pkg/app/options.go | 3 +- pkg/settings-check/hook.go | 165 +++++++--------------------- 5 files changed, 58 insertions(+), 136 deletions(-) diff --git a/examples/values-check/hooks/main.go b/examples/values-check/hooks/main.go index c2652b50..ff66fa1c 100644 --- a/examples/values-check/hooks/main.go +++ b/examples/values-check/hooks/main.go @@ -2,21 +2,28 @@ package main import ( "context" - "fmt" - "github.com/deckhouse/module-sdk/pkg" "github.com/deckhouse/module-sdk/pkg/app" + settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" ) const ( SnapshotKey = "apiservers" ) -func SettingsCheckFunc(ctx context.Context, input *pkg.HookInput) error { - fmt.Println("settings check") - return nil +func settingsCheckFunc(ctx context.Context, input *settingscheck.SettingsCheckHookInput) settingscheck.SettingsCheckHookResult { + res := settingscheck.SettingsCheckHookResult{ + Allow: true, + } + + if true { + res.Allow = false + res.Message = "this is a test warning" + } + + return res } func main() { - app.Run(app.WithSettingsCheck(SettingsCheckFunc)) + app.Run(app.WithSettingsCheck(settingsCheckFunc)) } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index b7521b6c..e70f176c 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -158,7 +158,7 @@ func (c *HookController) RunSettingsCheck(ctx context.Context) error { hook := c.registry.SettingsCheck() if hook == nil { - return ErrReadinessHookDoesNotExists + return ErrSettingsCheckHookDoesNotExists } transport := file.NewTransport(c.fConfig, hook.GetName(), c.dc, c.logger.Named("file-transport")) @@ -187,7 +187,7 @@ func (c *HookController) RunSettingsCheck(ctx context.Context) error { type SettingsCheckConfig struct { ModuleName string - ProbeFunc func(ctx context.Context, input *pkg.HookInput) error + ProbeFunc settingscheck.SettingsCheckFunc } func addSettingsCheckHook(reg *registry.HookRegistry, cfg *SettingsCheckConfig) { diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go index 974bac9f..c0d5c882 100644 --- a/pkg/app/app_config.go +++ b/pkg/app/app_config.go @@ -10,6 +10,7 @@ import ( "github.com/deckhouse/module-sdk/internal/controller" "github.com/deckhouse/module-sdk/pkg" + settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" ) type hookConfig struct { @@ -41,7 +42,7 @@ type readinessConfig struct { } type settingsCheckConfig struct { - ProbeFunc func(ctx context.Context, input *pkg.HookInput) error + ProbeFunc settingscheck.SettingsCheckFunc } type config struct { diff --git a/pkg/app/options.go b/pkg/app/options.go index 2c7cc01e..43de0593 100644 --- a/pkg/app/options.go +++ b/pkg/app/options.go @@ -4,6 +4,7 @@ import ( "context" "github.com/deckhouse/module-sdk/pkg" + settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" ) // RunConfigOption is a function that modifies the configuration for Run. @@ -33,7 +34,7 @@ type ReadinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -func WithSettingsCheck(probeFunc settingsProbeFunc) RunConfigOption { +func WithSettingsCheck(probeFunc settingscheck.SettingsCheckFunc) RunConfigOption { if probeFunc == nil { return func(c *config) {} } diff --git a/pkg/settings-check/hook.go b/pkg/settings-check/hook.go index 8b61b1e1..12d11117 100644 --- a/pkg/settings-check/hook.go +++ b/pkg/settings-check/hook.go @@ -18,15 +18,10 @@ package settingscheck import ( "context" - "errors" "fmt" "log/slog" "github.com/deckhouse/module-sdk/pkg" - objectpatch "github.com/deckhouse/module-sdk/pkg/object-patch" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -41,7 +36,7 @@ func GetModuleGVR() *schema.GroupVersionResource { type SettingsCheckHookConfig struct { ModuleName string - ProbeFunc func(ctx context.Context, input *pkg.HookInput) error + ProbeFunc SettingsCheckFunc } func NewSettingsCheckHookEM(cfg *SettingsCheckHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { @@ -49,7 +44,7 @@ func NewSettingsCheckHookEM(cfg *SettingsCheckHookConfig) (*pkg.HookConfig, pkg. panic("empty readiness config") } - return NewSettingsCheckConfig(cfg), ModuleSettingsCheck(cfg) + return NewSettingsCheckConfig(cfg), SettingsCheck(cfg) } func NewSettingsCheckConfig(cfg *SettingsCheckHookConfig) *pkg.HookConfig { @@ -62,146 +57,64 @@ func NewSettingsCheckConfig(cfg *SettingsCheckHookConfig) *pkg.HookConfig { } } -const ( - conditionStatusIsReady = "IsReady" - modulePhaseReconciling = "Reconciling" - modulePhaseReady = "Ready" - modulePhaseHookError = "Error" -) +// const ( +// conditionStatusIsReady = "IsReady" +// modulePhaseReconciling = "Reconciling" +// modulePhaseReady = "Ready" +// modulePhaseHookError = "Error" +// ) + +type SettingsCheckHookResult struct { + Allow bool + Message string +} + +type SettingsCheckHookInput struct { + Values pkg.OutputPatchableValuesCollector + Logger pkg.Logger + DC pkg.DependencyContainer +} + +type SettingsCheckFunc func(ctx context.Context, input *SettingsCheckHookInput) SettingsCheckHookResult -func ModuleSettingsCheck(cfg *SettingsCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { +func SettingsCheck(cfg *SettingsCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { if cfg.ModuleName == "" { - panic("empty readiness module name") + panic("empty module name") } if cfg.ProbeFunc == nil { - cfg.ProbeFunc = func(_ context.Context, input *pkg.HookInput) error { + cfg.ProbeFunc = func(_ context.Context, input *SettingsCheckHookInput) SettingsCheckHookResult { input.Logger.Info("default probe function") - return nil + return SettingsCheckHookResult{ + Allow: false, + } } } return func(ctx context.Context, input *pkg.HookInput) error { logger := input.Logger.With(slog.String("module", cfg.ModuleName)) - logger.Info("check readiness") + logger.Info("check settings") - k8sClient, err := input.DC.GetK8sClient() - if err != nil { - return fmt.Errorf("get k8s client: %w", err) - } + // here we may check if the module is ready to serve requests - uModule, err := k8sClient.Dynamic().Resource(*GetModuleGVR()).Get(ctx, cfg.ModuleName, metav1.GetOptions{}) - if err != nil { - return fmt.Errorf("get module resource: %w", err) + probeInput := &SettingsCheckHookInput{ + Values: input.ConfigValues, + Logger: logger, + DC: input.DC, } - if uModule == nil { - return errors.New("unstructured object is nil") - } - - // Get conditions - uConditions, ok, err := unstructured.NestedSlice(uModule.Object, "status", "conditions") - if err != nil { - return fmt.Errorf("nested slice: failed to get status.conditions: %w", err) - } + result := cfg.ProbeFunc(ctx, probeInput) - if !ok { - return errors.New("can't find status.conditions") + if !result.Allow { + return fmt.Errorf("settings check failed: %s", result.Message) } - if len(uConditions) == 0 { - return errors.New("status.conditions is empty") + // if allow with message, warn about it + if result.Message != "" { + logger.Warn(result.Message) } - phase, ok, err := unstructured.NestedString(uModule.Object, "status", "phase") - if err != nil { - return fmt.Errorf("nested string: failed to get status.phase: %w", err) - } - - if !ok { - return errors.New("can't find status.phase") - } - - if phase != modulePhaseReconciling && phase != modulePhaseReady && phase != modulePhaseHookError { - logger.Debug("waiting for sustainable phase", slog.String("phase", phase)) - - return nil - } - - // Run probe and get status - probeStatus := string(corev1.ConditionTrue) - probeMessage := "" - probePhase := modulePhaseReady - probeReason := "" - if err := cfg.ProbeFunc(ctx, input); err != nil { - probeStatus = string(corev1.ConditionFalse) - probeMessage = err.Error() - probePhase = modulePhaseReconciling - probeReason = "ReadinessProbeFailed" - } - - // search IsReady condition - condIdx := -1 - var cond map[string]interface{} - - for idx, rawCond := range uConditions { - cond = rawCond.(map[string]interface{}) - if cond["type"].(string) == conditionStatusIsReady { - condIdx = idx - break - } - } - - if condIdx < 0 { - cond["type"] = conditionStatusIsReady - uConditions = append(uConditions, cond) - condIdx = len(uConditions) - 1 - } - - cond["lastProbeTime"] = input.DC.GetClock().Now().Format("2006-01-02T15:04:05Z") - - if cond["message"] != probeMessage || probePhase != phase { - // if probe status changed - update time - if probeStatus != cond["status"] { - cond["lastTransitionTime"] = input.DC.GetClock().Now().Format("2006-01-02T15:04:05Z") - } - - cond["status"] = probeStatus - - cond["message"] = probeMessage - if probeMessage == "" { - delete(cond, "message") - } - - cond["reason"] = probeReason - if probeReason == "" { - delete(cond, "reason") - } - - // Update module status phase - phase = probePhase - } - - uConditions[condIdx] = cond - - // creating patch - patch := map[string]any{ - "status": map[string]any{ - "conditions": uConditions, - "phase": phase, - }, - } - - input.PatchCollector.PatchWithMerge( - patch, - GetModuleGVR().GroupVersion().String(), - "Module", - "", - cfg.ModuleName, - objectpatch.WithSubresource("/status"), - ) - return nil } } From 52b4f4c69dfb66e88d3a3a1c7577591f28f18ce1 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Tue, 9 Dec 2025 15:28:25 +0300 Subject: [PATCH 04/10] fix lint Signed-off-by: Smyslov Maxim --- examples/values-check/hooks/main.go | 10 +- pkg/app/options.go | 6 +- pkg/settings-check/hook.go | 10 +- pkg/settings-check/hook_test.go | 293 ---------------------------- 4 files changed, 9 insertions(+), 310 deletions(-) delete mode 100644 pkg/settings-check/hook_test.go diff --git a/examples/values-check/hooks/main.go b/examples/values-check/hooks/main.go index ff66fa1c..b8750f9e 100644 --- a/examples/values-check/hooks/main.go +++ b/examples/values-check/hooks/main.go @@ -7,16 +7,14 @@ import ( settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" ) -const ( - SnapshotKey = "apiservers" -) - -func settingsCheckFunc(ctx context.Context, input *settingscheck.SettingsCheckHookInput) settingscheck.SettingsCheckHookResult { +func settingsCheckFunc(_ context.Context, input *settingscheck.SettingsCheckHookInput) settingscheck.SettingsCheckHookResult { res := settingscheck.SettingsCheckHookResult{ Allow: true, } - if true { + test := input.Values.Get("global.test").Bool() + + if !test { res.Allow = false res.Message = "this is a test warning" } diff --git a/pkg/app/options.go b/pkg/app/options.go index 43de0593..1071bdc9 100644 --- a/pkg/app/options.go +++ b/pkg/app/options.go @@ -36,7 +36,9 @@ type ReadinessConfig struct { func WithSettingsCheck(probeFunc settingscheck.SettingsCheckFunc) RunConfigOption { if probeFunc == nil { - return func(c *config) {} + return func(c *config) { + c.SettingsCheckConfig = nil + } } return func(c *config) { @@ -45,5 +47,3 @@ func WithSettingsCheck(probeFunc settingscheck.SettingsCheckFunc) RunConfigOptio } } } - -type settingsProbeFunc func(ctx context.Context, input *pkg.HookInput) error diff --git a/pkg/settings-check/hook.go b/pkg/settings-check/hook.go index 12d11117..e9ebbd34 100644 --- a/pkg/settings-check/hook.go +++ b/pkg/settings-check/hook.go @@ -21,8 +21,9 @@ import ( "fmt" "log/slog" - "github.com/deckhouse/module-sdk/pkg" "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/deckhouse/module-sdk/pkg" ) func GetModuleGVR() *schema.GroupVersionResource { @@ -57,13 +58,6 @@ func NewSettingsCheckConfig(cfg *SettingsCheckHookConfig) *pkg.HookConfig { } } -// const ( -// conditionStatusIsReady = "IsReady" -// modulePhaseReconciling = "Reconciling" -// modulePhaseReady = "Ready" -// modulePhaseHookError = "Error" -// ) - type SettingsCheckHookResult struct { Allow bool Message string diff --git a/pkg/settings-check/hook_test.go b/pkg/settings-check/hook_test.go deleted file mode 100644 index 5d66af33..00000000 --- a/pkg/settings-check/hook_test.go +++ /dev/null @@ -1,293 +0,0 @@ -// /* -// Copyright 2025 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 settingscheck_test - -// import ( -// "context" -// "errors" -// "fmt" -// "testing" -// "time" - -// "github.com/gojuno/minimock/v3" -// "github.com/jonboulle/clockwork" -// "github.com/stretchr/testify/assert" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - -// "github.com/deckhouse/deckhouse/pkg/log" - -// "github.com/deckhouse/module-sdk/internal/common-hooks/readiness" -// "github.com/deckhouse/module-sdk/pkg" -// mock "github.com/deckhouse/module-sdk/testing/mock" -// ) - -// func Test_ReadinessHookConfig(t *testing.T) { -// t.Run("config is valid", func(t *testing.T) { -// assert.NoError(t, readiness.NewReadinessConfig(&readiness.ReadinessHookConfig{}).Validate()) -// }) -// } - -// func Test_CheckModuleReadiness(t *testing.T) { -// t.Run("successful check", func(t *testing.T) { -// mc := minimock.NewController(t) -// defer mc.Cleanup(func() {}) - -// dc := mock.NewDependencyContainerMock(mc) -// patchCollector := mock.NewPatchCollectorMock(t) - -// resource := &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "status": map[string]interface{}{ -// "conditions": []interface{}{ -// map[string]interface{}{ -// "type": "IsReady", -// "status": "False", -// "message": "Module is not ready", -// "lastTransitionTime": "2005-01-02T15:04:05Z", -// }, -// }, -// "phase": "Reconciling", -// }, -// }, -// } - -// patch := map[string]any{ -// "status": map[string]interface{}{ -// "conditions": []interface{}{ -// map[string]interface{}{ -// "type": "IsReady", -// "status": "True", -// "lastTransitionTime": "2006-01-02T15:04:05Z", -// "lastProbeTime": "2006-01-02T15:04:05Z", -// }, -// }, -// "phase": "Ready", -// }, -// } - -// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) -// resourceMock.GetMock. -// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). -// Return(resource, nil) - -// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) -// dynamicClientMock.ResourceMock. -// Expect(*readiness.GetModuleGVR()). -// Return(resourceMock) - -// k8sClientMock := mock.NewKubernetesClientMock(mc) -// k8sClientMock.DynamicMock. -// Return(dynamicClientMock) - -// dc.GetK8sClientMock. -// Expect(). -// Return(k8sClientMock, nil) - -// patchCollector.PatchWithMergeMock. -// Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { -// assert.Equal(t, patch, mergePatch) -// assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) -// assert.Equal(t, kind, "Module") -// assert.Equal(t, namespace, "") -// assert.Equal(t, name, "stub") -// }) - -// clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") -// assert.NoError(t, err) - -// dc.GetClockMock. -// Expect(). -// Return(clockwork.NewFakeClockAt(clockTime)) - -// input := &pkg.HookInput{ -// DC: dc, -// PatchCollector: patchCollector, -// Logger: log.NewNop(), -// } - -// config := &readiness.ReadinessHookConfig{ -// ModuleName: "stub", -// IntervalInSeconds: 10, -// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { -// return nil -// }, -// } - -// err = readiness.CheckModuleReadiness(config)(context.Background(), input) -// assert.NoError(t, err) -// }) - -// t.Run("k8s client error", func(t *testing.T) { -// mc := minimock.NewController(t) -// defer mc.Cleanup(func() {}) - -// dc := mock.NewDependencyContainerMock(mc) -// dc.GetK8sClientMock. -// Expect(). -// Return(nil, fmt.Errorf("k8s client error")) - -// input := &pkg.HookInput{ -// DC: dc, -// Logger: log.NewNop(), -// } - -// config := &readiness.ReadinessHookConfig{ -// ModuleName: "stub", -// IntervalInSeconds: 10, -// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { -// return nil -// }, -// } - -// err := readiness.CheckModuleReadiness(config)(context.Background(), input) -// assert.Error(t, err) -// assert.Contains(t, err.Error(), "k8s client error") -// }) - -// t.Run("get resource error", func(t *testing.T) { -// mc := minimock.NewController(t) -// defer mc.Cleanup(func() {}) - -// dc := mock.NewDependencyContainerMock(mc) - -// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) -// resourceMock.GetMock. -// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). -// Return(nil, fmt.Errorf("get error")) - -// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) -// dynamicClientMock.ResourceMock. -// Expect(*readiness.GetModuleGVR()). -// Return(resourceMock) - -// k8sClientMock := mock.NewKubernetesClientMock(mc) -// k8sClientMock.DynamicMock. -// Return(dynamicClientMock) - -// dc.GetK8sClientMock. -// Expect(). -// Return(k8sClientMock, nil) - -// input := &pkg.HookInput{ -// DC: dc, -// Logger: log.NewNop(), -// } - -// config := &readiness.ReadinessHookConfig{ -// ModuleName: "stub", -// IntervalInSeconds: 10, -// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { -// return nil -// }, -// } - -// err := readiness.CheckModuleReadiness(config)(context.Background(), input) -// assert.Error(t, err) -// assert.Contains(t, err.Error(), "get error") -// }) - -// t.Run("readiness error", func(t *testing.T) { -// mc := minimock.NewController(t) -// defer mc.Cleanup(func() {}) - -// dc := mock.NewDependencyContainerMock(mc) -// patchCollector := mock.NewPatchCollectorMock(t) - -// resource := &unstructured.Unstructured{ -// Object: map[string]interface{}{ -// "status": map[string]interface{}{ -// "conditions": []interface{}{ -// map[string]interface{}{ -// "type": "IsReady", -// "status": "True", -// "lastTransitionTime": "2005-01-02T15:04:05Z", -// }, -// }, -// "phase": "Reconciling", -// }, -// }, -// } - -// patch := map[string]any{ -// "status": map[string]interface{}{ -// "conditions": []interface{}{ -// map[string]interface{}{ -// "type": "IsReady", -// "status": "False", -// "message": "readiness error", -// "reason": "ReadinessProbeFailed", -// "lastTransitionTime": "2006-01-02T15:04:05Z", -// "lastProbeTime": "2006-01-02T15:04:05Z", -// }, -// }, -// "phase": "Reconciling", -// }, -// } - -// resourceMock := mock.NewKubernetesNamespaceableResourceInterfaceMock(mc) -// resourceMock.GetMock. -// Expect(minimock.AnyContext, "stub", metav1.GetOptions{}). -// Return(resource, nil) - -// dynamicClientMock := mock.NewKubernetesDynamicClientMock(mc) -// dynamicClientMock.ResourceMock. -// Expect(*readiness.GetModuleGVR()). -// Return(resourceMock) - -// k8sClientMock := mock.NewKubernetesClientMock(mc) -// k8sClientMock.DynamicMock. -// Return(dynamicClientMock) - -// dc.GetK8sClientMock. -// Expect(). -// Return(k8sClientMock, nil) - -// patchCollector.PatchWithMergeMock. -// Set(func(mergePatch any, apiVersion, kind, namespace, name string, _ ...pkg.PatchCollectorOption) { -// assert.Equal(t, patch, mergePatch) -// assert.Equal(t, apiVersion, readiness.GetModuleGVR().GroupVersion().String()) -// assert.Equal(t, kind, "Module") -// assert.Equal(t, namespace, "") -// assert.Equal(t, name, "stub") -// }) - -// clockTime, err := time.Parse(time.DateTime, "2006-01-02 15:04:05") -// assert.NoError(t, err) - -// dc.GetClockMock. -// Expect(). -// Return(clockwork.NewFakeClockAt(clockTime)) - -// input := &pkg.HookInput{ -// DC: dc, -// PatchCollector: patchCollector, -// Logger: log.NewNop(), -// } - -// config := &readiness.ReadinessHookConfig{ -// ModuleName: "stub", -// IntervalInSeconds: 10, -// ProbeFunc: func(_ context.Context, _ *pkg.HookInput) error { -// return errors.New("readiness error") -// }, -// } - -// err = readiness.CheckModuleReadiness(config)(context.Background(), input) -// assert.NoError(t, err) -// }) -// } From 4a30b394802c2ee99ada02180f71816271163d93 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Tue, 9 Dec 2025 16:04:53 +0300 Subject: [PATCH 05/10] [chore] check settings Signed-off-by: Stepan Paksashvili --- .../hooks/go.mod | 0 .../hooks/go.sum | 0 examples/settings-check/hooks/main.go | 20 +++ examples/values-check/hooks/main.go | 27 ---- internal/controller/config.go | 9 +- internal/controller/controller.go | 77 ++++-------- internal/registry/registry.go | 16 +-- pkg/app/app_config.go | 23 ++-- pkg/app/options.go | 14 +-- pkg/app/root.go | 18 ++- pkg/hook/dto.go | 7 +- pkg/patch.go | 9 ++ pkg/settings-check/hook.go | 114 ----------------- pkg/settingscheck/check.go | 119 ++++++++++++++++++ 14 files changed, 199 insertions(+), 254 deletions(-) rename examples/{values-check => settings-check}/hooks/go.mod (100%) rename examples/{values-check => settings-check}/hooks/go.sum (100%) create mode 100644 examples/settings-check/hooks/main.go delete mode 100644 examples/values-check/hooks/main.go delete mode 100644 pkg/settings-check/hook.go create mode 100644 pkg/settingscheck/check.go diff --git a/examples/values-check/hooks/go.mod b/examples/settings-check/hooks/go.mod similarity index 100% rename from examples/values-check/hooks/go.mod rename to examples/settings-check/hooks/go.mod diff --git a/examples/values-check/hooks/go.sum b/examples/settings-check/hooks/go.sum similarity index 100% rename from examples/values-check/hooks/go.sum rename to examples/settings-check/hooks/go.sum diff --git a/examples/settings-check/hooks/main.go b/examples/settings-check/hooks/main.go new file mode 100644 index 00000000..eee035fb --- /dev/null +++ b/examples/settings-check/hooks/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + + "github.com/deckhouse/module-sdk/pkg/app" + "github.com/deckhouse/module-sdk/pkg/settingscheck" +) + +func settingsCheck(_ context.Context, input settingscheck.Input) error { + if !input.Settings.Get(".enabled").Bool() { + return &settingscheck.Warning{Message: "settings disabled"} + } + + return nil +} + +func main() { + app.Run(app.WithSettingsCheck(settingsCheck)) +} diff --git a/examples/values-check/hooks/main.go b/examples/values-check/hooks/main.go deleted file mode 100644 index b8750f9e..00000000 --- a/examples/values-check/hooks/main.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "context" - - "github.com/deckhouse/module-sdk/pkg/app" - settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" -) - -func settingsCheckFunc(_ context.Context, input *settingscheck.SettingsCheckHookInput) settingscheck.SettingsCheckHookResult { - res := settingscheck.SettingsCheckHookResult{ - Allow: true, - } - - test := input.Values.Get("global.test").Bool() - - if !test { - res.Allow = false - res.Message = "this is a test warning" - } - - return res -} - -func main() { - app.Run(app.WithSettingsCheck(settingsCheckFunc)) -} diff --git a/internal/controller/config.go b/internal/controller/config.go index 152a977a..cc069801 100644 --- a/internal/controller/config.go +++ b/internal/controller/config.go @@ -7,6 +7,7 @@ import ( "github.com/deckhouse/module-sdk/internal/transport/file" "github.com/deckhouse/module-sdk/pkg" + "github.com/deckhouse/module-sdk/pkg/settingscheck" ) type HookConfig struct { @@ -32,10 +33,10 @@ type ReadinessConfig struct { } type Config struct { - ModuleName string - HookConfig *HookConfig - ReadinessConfig *ReadinessConfig - SettingsCheckConfig *SettingsCheckConfig + ModuleName string + HookConfig *HookConfig + ReadinessConfig *ReadinessConfig + SettingsCheck settingscheck.Check LogLevelRaw string LogLevel log.Level diff --git a/internal/controller/controller.go b/internal/controller/controller.go index e70f176c..73ce2182 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -18,16 +18,17 @@ import ( "github.com/deckhouse/module-sdk/pkg/dependency" gohook "github.com/deckhouse/module-sdk/pkg/hook" outerRegistry "github.com/deckhouse/module-sdk/pkg/registry" - settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" + "github.com/deckhouse/module-sdk/pkg/settingscheck" "github.com/deckhouse/module-sdk/pkg/utils/ptr" ) type HookController struct { registry *registry.HookRegistry - dc pkg.DependencyContainer + fConfig *file.Config - fConfig *file.Config + settingsCheck settingscheck.Check + dc pkg.DependencyContainer logger *log.Logger } @@ -46,15 +47,12 @@ func NewHookController(cfg *Config, logger *log.Logger) *HookController { addReadinessHook(reg, cfg.ReadinessConfig) } - if cfg.SettingsCheckConfig != nil { - addSettingsCheckHook(reg, cfg.SettingsCheckConfig) - } - return &HookController{ - registry: reg, - dc: dependency.NewDependencyContainer(), - fConfig: cfg.GetFileConfig(), - logger: logger, + registry: reg, + settingsCheck: cfg.SettingsCheck, + dc: dependency.NewDependencyContainer(), + fConfig: cfg.GetFileConfig(), + logger: logger, } } @@ -152,61 +150,24 @@ func (c *HookController) RunReadiness(ctx context.Context) error { return nil } -var ErrSettingsCheckHookDoesNotExists = errors.New("settings check hook does not exists") - -func (c *HookController) RunSettingsCheck(ctx context.Context) error { - hook := c.registry.SettingsCheck() - - if hook == nil { - return ErrSettingsCheckHookDoesNotExists - } - - transport := file.NewTransport(c.fConfig, hook.GetName(), c.dc, c.logger.Named("file-transport")) - - hookRes, err := hook.Execute(ctx, transport.NewRequest()) - if err != nil { - outputError := &gohook.Error{Message: "execute: " + err.Error()} - - buf := bytes.NewBuffer([]byte{}) - err := json.NewEncoder(buf).Encode(outputError) - if err != nil { - return fmt.Errorf("encode error: %w", err) - } +func (c *HookController) CheckSettings(ctx context.Context) error { + res := settingscheck.Wrap(ctx, c.settingsCheck, c.dc, c.logger) - fmt.Fprintln(os.Stderr, buf.String()) - os.Exit(1) + buf := bytes.NewBuffer([]byte{}) + if err := json.NewEncoder(buf).Encode(res); err != nil { + return fmt.Errorf("encode error: %w", err) } - err = transport.NewResponse().Send(hookRes) - if err != nil { - return fmt.Errorf("send: %w", err) - } + fmt.Fprintln(os.Stderr, buf.String()) + os.Exit(1) return nil } -type SettingsCheckConfig struct { - ModuleName string - ProbeFunc settingscheck.SettingsCheckFunc -} - -func addSettingsCheckHook(reg *registry.HookRegistry, cfg *SettingsCheckConfig) { - settingsCheckConfig := &settingscheck.SettingsCheckHookConfig{ - ModuleName: cfg.ModuleName, - ProbeFunc: cfg.ProbeFunc, - } - - config, f := settingscheck.NewSettingsCheckHookEM(settingsCheckConfig) - config.Metadata.Name = "settings-check" - config.Metadata.Path = "common-hooks/settings-check" - - reg.SetSettingsCheckHook(&pkg.Hook{Config: config, ReconcileFunc: f}) -} - var ErrNoHooksRegistered = errors.New("no hooks registered") func (c *HookController) PrintHookConfigs() error { - if len(c.registry.Hooks()) == 0 { + if len(c.registry.Hooks()) == 0 && c.settingsCheck == nil { return ErrNoHooksRegistered } @@ -225,6 +186,10 @@ func (c *HookController) PrintHookConfigs() error { cfg.Readiness = remapHookConfigToHookConfig(c.registry.Readiness().GetConfig()) } + if c.settingsCheck != nil { + cfg.HasSettingsCheck = true + } + buf := bytes.NewBuffer([]byte{}) err := json.NewEncoder(buf).Encode(cfg) if err != nil { diff --git a/internal/registry/registry.go b/internal/registry/registry.go index 893ee405..8b5a59b1 100644 --- a/internal/registry/registry.go +++ b/internal/registry/registry.go @@ -8,9 +8,8 @@ import ( ) type HookRegistry struct { - hooks []*gohook.Hook - readinessHook *gohook.Hook - settingsCheckHook *gohook.Hook + hooks []*gohook.Hook + readinessHook *gohook.Hook logger *log.Logger } @@ -35,10 +34,6 @@ func (h *HookRegistry) Readiness() *gohook.Hook { return h.readinessHook } -func (h *HookRegistry) SettingsCheck() *gohook.Hook { - return h.settingsCheckHook -} - func (h *HookRegistry) Add(hooks ...*pkg.Hook) { for _, hook := range hooks { newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) @@ -54,10 +49,3 @@ func (h *HookRegistry) SetReadinessHook(hook *pkg.Hook) { h.readinessHook = newHook } - -func (h *HookRegistry) SetSettingsCheckHook(hook *pkg.Hook) { - newHook := gohook.NewHook(hook.Config, hook.ReconcileFunc) - newHook.SetLogger(h.logger.Named(newHook.GetName())) - - h.settingsCheckHook = newHook -} diff --git a/pkg/app/app_config.go b/pkg/app/app_config.go index c0d5c882..29dcb1a5 100644 --- a/pkg/app/app_config.go +++ b/pkg/app/app_config.go @@ -4,13 +4,13 @@ import ( "context" "fmt" - env "github.com/caarlos0/env/v11" + "github.com/caarlos0/env/v11" "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/module-sdk/internal/controller" "github.com/deckhouse/module-sdk/pkg" - settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" + "github.com/deckhouse/module-sdk/pkg/settingscheck" ) type hookConfig struct { @@ -41,15 +41,11 @@ type readinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -type settingsCheckConfig struct { - ProbeFunc settingscheck.SettingsCheckFunc -} - type config struct { - ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` - HookConfig *hookConfig - ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` - SettingsCheckConfig *settingsCheckConfig + ModuleName string `env:"MODULE_NAME" envDefault:"default-module"` + HookConfig *hookConfig + ReadinessConfig *readinessConfig `envPrefix:"READINESS_"` + SettingsCheck settingscheck.Check LogLevelRaw string `env:"LOG_LEVEL" envDefault:"FATAL"` LogLevel log.Level `env:"-"` @@ -103,11 +99,8 @@ func remapConfigToControllerConfig(input *config) *controller.Config { } } - if input.SettingsCheckConfig != nil { - cfg.SettingsCheckConfig = &controller.SettingsCheckConfig{ - ModuleName: input.ModuleName, - ProbeFunc: input.SettingsCheckConfig.ProbeFunc, - } + if input.SettingsCheck != nil { + cfg.SettingsCheck = input.SettingsCheck } return cfg diff --git a/pkg/app/options.go b/pkg/app/options.go index 1071bdc9..7b446691 100644 --- a/pkg/app/options.go +++ b/pkg/app/options.go @@ -4,7 +4,7 @@ import ( "context" "github.com/deckhouse/module-sdk/pkg" - settingscheck "github.com/deckhouse/module-sdk/pkg/settings-check" + "github.com/deckhouse/module-sdk/pkg/settingscheck" ) // RunConfigOption is a function that modifies the configuration for Run. @@ -34,16 +34,8 @@ type ReadinessConfig struct { ProbeFunc func(ctx context.Context, input *pkg.HookInput) error } -func WithSettingsCheck(probeFunc settingscheck.SettingsCheckFunc) RunConfigOption { - if probeFunc == nil { - return func(c *config) { - c.SettingsCheckConfig = nil - } - } - +func WithSettingsCheck(check settingscheck.Check) RunConfigOption { return func(c *config) { - c.SettingsCheckConfig = &settingsCheckConfig{ - ProbeFunc: probeFunc, - } + c.SettingsCheck = check } } diff --git a/pkg/app/root.go b/pkg/app/root.go index 318a0b41..425b18f3 100644 --- a/pkg/app/root.go +++ b/pkg/app/root.go @@ -78,21 +78,20 @@ func (c *cmd) hooksCmd() *cobra.Command { }, }) - settingsCheckCmd := &cobra.Command{ + hooksCmd.AddCommand(&cobra.Command{ Use: "check", Short: "Check settings", Long: `Check settings of the module`, Hidden: true, - Run: func(cmd *cobra.Command, _ []string) { - ctx := cmd.Context() - - err := c.controller.RunSettingsCheck(ctx) - if err != nil { - c.logger.Warn("settings check hook shutdown", "error", err) - os.Exit(1) + RunE: func(cmd *cobra.Command, _ []string) error { + if err := c.controller.CheckSettings(cmd.Context()); err != nil { + c.logger.Error("failed to check settings", "error", err) + return err } + + return nil }, - } + }) configCmd := &cobra.Command{ Use: "config", @@ -108,7 +107,6 @@ func (c *cmd) hooksCmd() *cobra.Command { return nil }, } - configCmd.AddCommand(settingsCheckCmd) hooksCmd.AddCommand(configCmd) dumpCmd := &cobra.Command{ diff --git a/pkg/hook/dto.go b/pkg/hook/dto.go index fe64fa4c..2cfe6e98 100644 --- a/pkg/hook/dto.go +++ b/pkg/hook/dto.go @@ -18,9 +18,10 @@ type GoHookMetadata struct { const BatchHookConfigV1 = "v1" type BatchHookConfig struct { - Version string `yaml:"version" json:"version"` - Hooks []HookConfig `yaml:"hooks" json:"hooks"` - Readiness *HookConfig `yaml:"readiness,omitempty" json:"readiness,omitempty"` + Version string `yaml:"version" json:"version"` + Hooks []HookConfig `yaml:"hooks" json:"hooks"` + Readiness *HookConfig `yaml:"readiness,omitempty" json:"readiness,omitempty"` + HasSettingsCheck bool `yaml:"has_settings_check,omitempty" json:"has_settings_check,omitempty"` } type HookConfig struct { diff --git a/pkg/patch.go b/pkg/patch.go index 1f634e27..7d657fe9 100644 --- a/pkg/patch.go +++ b/pkg/patch.go @@ -91,3 +91,12 @@ type PatchableValuesCollector interface { Remove(path string) Set(path string, value any) } + +type ReadOnlyValuesCollector interface { + ArrayCount(path string) (int, error) + Exists(path string) bool + Get(path string) gjson.Result + GetOk(path string) (gjson.Result, bool) + GetPatches() []*utils.ValuesPatchOperation + GetRaw(path string) any +} diff --git a/pkg/settings-check/hook.go b/pkg/settings-check/hook.go deleted file mode 100644 index e9ebbd34..00000000 --- a/pkg/settings-check/hook.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright 2022 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 settingscheck - -import ( - "context" - "fmt" - "log/slog" - - "k8s.io/apimachinery/pkg/runtime/schema" - - "github.com/deckhouse/module-sdk/pkg" -) - -func GetModuleGVR() *schema.GroupVersionResource { - // ModuleGVR GroupVersionResource - return &schema.GroupVersionResource{ - Group: "deckhouse.io", - Version: "v1alpha1", - Resource: "modules", - } -} - -type SettingsCheckHookConfig struct { - ModuleName string - ProbeFunc SettingsCheckFunc -} - -func NewSettingsCheckHookEM(cfg *SettingsCheckHookConfig) (*pkg.HookConfig, pkg.ReconcileFunc) { - if cfg == nil { - panic("empty readiness config") - } - - return NewSettingsCheckConfig(cfg), SettingsCheck(cfg) -} - -func NewSettingsCheckConfig(cfg *SettingsCheckHookConfig) *pkg.HookConfig { - return &pkg.HookConfig{ - Schedule: []pkg.ScheduleConfig{ - { - Name: cfg.ModuleName + "-moduleReadinessSchedule", - }, - }, - } -} - -type SettingsCheckHookResult struct { - Allow bool - Message string -} - -type SettingsCheckHookInput struct { - Values pkg.OutputPatchableValuesCollector - Logger pkg.Logger - DC pkg.DependencyContainer -} - -type SettingsCheckFunc func(ctx context.Context, input *SettingsCheckHookInput) SettingsCheckHookResult - -func SettingsCheck(cfg *SettingsCheckHookConfig) func(ctx context.Context, input *pkg.HookInput) error { - if cfg.ModuleName == "" { - panic("empty module name") - } - - if cfg.ProbeFunc == nil { - cfg.ProbeFunc = func(_ context.Context, input *SettingsCheckHookInput) SettingsCheckHookResult { - input.Logger.Info("default probe function") - - return SettingsCheckHookResult{ - Allow: false, - } - } - } - - return func(ctx context.Context, input *pkg.HookInput) error { - logger := input.Logger.With(slog.String("module", cfg.ModuleName)) - logger.Info("check settings") - - // here we may check if the module is ready to serve requests - - probeInput := &SettingsCheckHookInput{ - Values: input.ConfigValues, - Logger: logger, - DC: input.DC, - } - - result := cfg.ProbeFunc(ctx, probeInput) - - if !result.Allow { - return fmt.Errorf("settings check failed: %s", result.Message) - } - - // if allow with message, warn about it - if result.Message != "" { - logger.Warn(result.Message) - } - - return nil - } -} diff --git a/pkg/settingscheck/check.go b/pkg/settingscheck/check.go new file mode 100644 index 00000000..88c487a5 --- /dev/null +++ b/pkg/settingscheck/check.go @@ -0,0 +1,119 @@ +/* +Copyright 2025 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 settingscheck + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/deckhouse/module-sdk/pkg" + patchablevalues "github.com/deckhouse/module-sdk/pkg/patchable-values" + "github.com/deckhouse/module-sdk/pkg/utils" +) + +const ( + EnvSettingsPath = "SETTINGS_PATH" +) + +type Result struct { + Allow bool `json:"allow" yaml:"allow"` + Warning string `json:"warning" yaml:"warning"` +} + +type Warning struct { + Message string `json:"message" yaml:"message"` +} + +func (w *Warning) Error() string { + return w.Message +} + +type Input struct { + Settings pkg.ReadOnlyValuesCollector + DC pkg.DependencyContainer + Logger pkg.Logger +} + +type Check func(ctx context.Context, input Input) error + +func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger pkg.Logger) Result { + if check == nil { + return Result{ + Allow: true, + } + } + + path := os.Getenv(EnvSettingsPath) + if path == "" { + return Result{ + Allow: false, + Warning: fmt.Sprintf("env '%s' not set", EnvSettingsPath), + } + } + + raw, err := os.ReadFile(path) + if err != nil { + return Result{ + Allow: false, + Warning: fmt.Sprintf("failed to read settings: %v", err), + } + } + + values, err := utils.NewValuesFromBytes(raw) + if err != nil { + return Result{ + Allow: false, + Warning: fmt.Sprintf("failed to parse settings: %v", err), + } + } + + settings, err := patchablevalues.NewPatchableValues(values) + if err != nil { + return Result{ + Allow: false, + Warning: fmt.Sprintf("failed to parse settings: %v", err), + } + } + + input := Input{ + Settings: settings, + DC: dc, + Logger: logger, + } + + err = check(ctx, input) + if err == nil { + return Result{ + Allow: true, + } + } + + warning := new(Warning) + if errors.As(err, &warning) { + return Result{ + Allow: true, + Warning: warning.Message, + } + } + + return Result{ + Allow: false, + Warning: err.Error(), + } +} From 1e597e72804830506cc887fce50c5756751228e3 Mon Sep 17 00:00:00 2001 From: Smyslov Maxim Date: Mon, 15 Dec 2025 17:47:05 +0300 Subject: [PATCH 06/10] [docs] add settings-check example and update README Added a new example for module hooks demonstrating settings checking. Updated the main README to include a link to this new example. Signed-off-by: Smyslov Maxim --- examples/README.md | 2 ++ examples/settings-check/README.md | 42 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 examples/settings-check/README.md diff --git a/examples/README.md b/examples/README.md index 657cb5d2..8854ec0a 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,3 +11,5 @@ [Module hook examples with common hooks](https://github.com/deckhouse/module-sdk/tree/main/examples/common-hooks) [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 diff --git a/examples/settings-check/README.md b/examples/settings-check/README.md new file mode 100644 index 00000000..cd747079 --- /dev/null +++ b/examples/settings-check/README.md @@ -0,0 +1,42 @@ +# Module Hooks Example +In this example you can build your hook binary with settings checkiung and check how it works. + +It can be usefull to understand how to do helm values validation. + +### 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 run settings check +```bash +go run . hook check +``` + +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-module-hooks . +``` \ No newline at end of file From 6dcc89042d060ef6ea52d2ae5b9bc9af3fa20113 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Tue, 16 Dec 2025 15:27:56 +0300 Subject: [PATCH 07/10] [chore] check settings Signed-off-by: Stepan Paksashvili --- examples/settings-check/hooks/main.go | 19 ++++++--- internal/controller/controller.go | 2 +- pkg/settingscheck/check.go | 61 ++++++++++++++------------- 3 files changed, 46 insertions(+), 36 deletions(-) diff --git a/examples/settings-check/hooks/main.go b/examples/settings-check/hooks/main.go index eee035fb..c16dbde8 100644 --- a/examples/settings-check/hooks/main.go +++ b/examples/settings-check/hooks/main.go @@ -7,14 +7,23 @@ import ( "github.com/deckhouse/module-sdk/pkg/settingscheck" ) -func settingsCheck(_ context.Context, input settingscheck.Input) error { - if !input.Settings.Get(".enabled").Bool() { - return &settingscheck.Warning{Message: "settings disabled"} +func check(_ context.Context, input settingscheck.Input) settingscheck.Result { + replicas := input.Settings.Get("replicas").Int() + if replicas == 0 { + return settingscheck.Reject("replicas cannot be 0") } - return nil + if replicas == 2 { + return settingscheck.Warn("replicas cannot be greater than 2") + } + + if replicas > 3 { + return settingscheck.Reject("replicas cannot be greater than 3") + } + + return settingscheck.Allow() } func main() { - app.Run(app.WithSettingsCheck(settingsCheck)) + app.Run(app.WithSettingsCheck(check)) } diff --git a/internal/controller/controller.go b/internal/controller/controller.go index 73ce2182..96a405a6 100644 --- a/internal/controller/controller.go +++ b/internal/controller/controller.go @@ -151,7 +151,7 @@ func (c *HookController) RunReadiness(ctx context.Context) error { } func (c *HookController) CheckSettings(ctx context.Context) error { - res := settingscheck.Wrap(ctx, c.settingsCheck, c.dc, c.logger) + res := settingscheck.Execute(ctx, c.settingsCheck, c.dc, c.logger) buf := bytes.NewBuffer([]byte{}) if err := json.NewEncoder(buf).Encode(res); err != nil { diff --git a/pkg/settingscheck/check.go b/pkg/settingscheck/check.go index 88c487a5..c9731ae2 100644 --- a/pkg/settingscheck/check.go +++ b/pkg/settingscheck/check.go @@ -18,7 +18,6 @@ package settingscheck import ( "context" - "errors" "fmt" "os" @@ -32,16 +31,9 @@ const ( ) type Result struct { - Allow bool `json:"allow" yaml:"allow"` - Warning string `json:"warning" yaml:"warning"` -} - -type Warning struct { - Message string `json:"message" yaml:"message"` -} - -func (w *Warning) Error() string { - return w.Message + Allow bool `json:"allow" yaml:"allow"` + Message string `json:"message" yaml:"message"` + Warnings []string `json:"warnings" yaml:"warnings"` } type Input struct { @@ -50,9 +42,9 @@ type Input struct { Logger pkg.Logger } -type Check func(ctx context.Context, input Input) error +type Check func(ctx context.Context, input Input) Result -func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger pkg.Logger) Result { +func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logger pkg.Logger) Result { if check == nil { return Result{ Allow: true, @@ -63,7 +55,7 @@ func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger p if path == "" { return Result{ Allow: false, - Warning: fmt.Sprintf("env '%s' not set", EnvSettingsPath), + Message: fmt.Sprintf("env '%s' not set", EnvSettingsPath), } } @@ -71,7 +63,7 @@ func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger p if err != nil { return Result{ Allow: false, - Warning: fmt.Sprintf("failed to read settings: %v", err), + Message: fmt.Sprintf("failed to read settings: %v", err), } } @@ -79,7 +71,7 @@ func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger p if err != nil { return Result{ Allow: false, - Warning: fmt.Sprintf("failed to parse settings: %v", err), + Message: fmt.Sprintf("failed to parse settings: %v", err), } } @@ -87,7 +79,7 @@ func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger p if err != nil { return Result{ Allow: false, - Warning: fmt.Sprintf("failed to parse settings: %v", err), + Message: fmt.Sprintf("failed to parse settings: %v", err), } } @@ -97,23 +89,32 @@ func Wrap(ctx context.Context, check Check, dc pkg.DependencyContainer, logger p Logger: logger, } - err = check(ctx, input) - if err == nil { - return Result{ - Allow: true, - } - } + return check(ctx, input) +} - warning := new(Warning) - if errors.As(err, &warning) { - return Result{ - Allow: true, - Warning: warning.Message, - } +func Reject(msg string) Result { + return Result{ + Allow: false, + Message: msg, } +} +func RejectErr(err error) Result { return Result{ Allow: false, - Warning: err.Error(), + Message: err.Error(), + } +} + +func Allow() Result { + return Result{ + Allow: true, + } +} + +func Warn(warnings ...string) Result { + return Result{ + Allow: true, + Warnings: warnings, } } From c45654f3e547824f6551388162402c51780fbbbe Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Tue, 16 Dec 2025 15:31:27 +0300 Subject: [PATCH 08/10] [chore] check settings Signed-off-by: Stepan Paksashvili --- examples/settings-check/hooks/main.go | 5 +++-- pkg/settingscheck/check.go | 15 +-------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/examples/settings-check/hooks/main.go b/examples/settings-check/hooks/main.go index c16dbde8..5a4e9050 100644 --- a/examples/settings-check/hooks/main.go +++ b/examples/settings-check/hooks/main.go @@ -13,15 +13,16 @@ func check(_ context.Context, input settingscheck.Input) settingscheck.Result { return settingscheck.Reject("replicas cannot be 0") } + var warnings []string if replicas == 2 { - return settingscheck.Warn("replicas cannot be greater than 2") + warnings = append(warnings, "replicas cannot be greater than 3") } if replicas > 3 { return settingscheck.Reject("replicas cannot be greater than 3") } - return settingscheck.Allow() + return settingscheck.Allow(warnings...) } func main() { diff --git a/pkg/settingscheck/check.go b/pkg/settingscheck/check.go index c9731ae2..1d592a43 100644 --- a/pkg/settingscheck/check.go +++ b/pkg/settingscheck/check.go @@ -99,20 +99,7 @@ func Reject(msg string) Result { } } -func RejectErr(err error) Result { - return Result{ - Allow: false, - Message: err.Error(), - } -} - -func Allow() Result { - return Result{ - Allow: true, - } -} - -func Warn(warnings ...string) Result { +func Allow(warnings ...string) Result { return Result{ Allow: true, Warnings: warnings, From 0575dd4d736c84a7a6bba2d45d4da70fd2ed1b6b Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Wed, 17 Dec 2025 15:36:10 +0300 Subject: [PATCH 09/10] [chore] check settings Signed-off-by: Stepan Paksashvili --- pkg/settingscheck/check.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/settingscheck/check.go b/pkg/settingscheck/check.go index 1d592a43..54cdd5bd 100644 --- a/pkg/settingscheck/check.go +++ b/pkg/settingscheck/check.go @@ -31,7 +31,7 @@ const ( ) type Result struct { - Allow bool `json:"allow" yaml:"allow"` + Valid bool `json:"valid" yaml:"valid"` Message string `json:"message" yaml:"message"` Warnings []string `json:"warnings" yaml:"warnings"` } @@ -47,14 +47,14 @@ type Check func(ctx context.Context, input Input) Result func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logger pkg.Logger) Result { if check == nil { return Result{ - Allow: true, + Valid: true, } } path := os.Getenv(EnvSettingsPath) if path == "" { return Result{ - Allow: false, + Valid: false, Message: fmt.Sprintf("env '%s' not set", EnvSettingsPath), } } @@ -62,7 +62,7 @@ func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logge raw, err := os.ReadFile(path) if err != nil { return Result{ - Allow: false, + Valid: false, Message: fmt.Sprintf("failed to read settings: %v", err), } } @@ -70,7 +70,7 @@ func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logge values, err := utils.NewValuesFromBytes(raw) if err != nil { return Result{ - Allow: false, + Valid: false, Message: fmt.Sprintf("failed to parse settings: %v", err), } } @@ -78,7 +78,7 @@ func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logge settings, err := patchablevalues.NewPatchableValues(values) if err != nil { return Result{ - Allow: false, + Valid: false, Message: fmt.Sprintf("failed to parse settings: %v", err), } } @@ -94,14 +94,14 @@ func Execute(ctx context.Context, check Check, dc pkg.DependencyContainer, logge func Reject(msg string) Result { return Result{ - Allow: false, + Valid: false, Message: msg, } } func Allow(warnings ...string) Result { return Result{ - Allow: true, + Valid: true, Warnings: warnings, } } From 1983ee5af5d3f0b608c3ba84fa18526e8db2fb94 Mon Sep 17 00:00:00 2001 From: Stepan Paksashvili Date: Wed, 17 Dec 2025 16:00:03 +0300 Subject: [PATCH 10/10] [chore] check settings Signed-off-by: Stepan Paksashvili --- pkg/patch.go | 2 +- pkg/settingscheck/check.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/patch.go b/pkg/patch.go index 7d657fe9..42534e3b 100644 --- a/pkg/patch.go +++ b/pkg/patch.go @@ -92,7 +92,7 @@ type PatchableValuesCollector interface { Set(path string, value any) } -type ReadOnlyValuesCollector interface { +type ReadableValuesCollector interface { ArrayCount(path string) (int, error) Exists(path string) bool Get(path string) gjson.Result diff --git a/pkg/settingscheck/check.go b/pkg/settingscheck/check.go index 54cdd5bd..73591ae1 100644 --- a/pkg/settingscheck/check.go +++ b/pkg/settingscheck/check.go @@ -37,7 +37,7 @@ type Result struct { } type Input struct { - Settings pkg.ReadOnlyValuesCollector + Settings pkg.ReadableValuesCollector DC pkg.DependencyContainer Logger pkg.Logger }