diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index ee9af73..0da6919 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -27,17 +27,22 @@ jobs: e2e: # Gate: run only when the PR is labeled e2e/run. if: ${{ contains(github.event.pull_request.labels.*.name, 'e2e/run') }} + # storage-e2e main carries the commander provider, in-process connect (SSH + # tunnel + kubeconfig fetch) and bootstrap-side module enablement (merged in + # deckhouse/storage-e2e#25 + #33). uses: deckhouse/storage-e2e/.github/workflows/e2e.yml@main with: module_slug: sds-object # The e2e Go module lives under e2e/; tests are in e2e/tests/. module_path: e2e test_package: ./tests/ - # CI config: modulePullOverride is ${E2E_MODULE_IMAGE_TAG}, resolved by the - # enable-modules step to the PR image tag below. + # CI config: modulePullOverride is ${E2E_MODULE_IMAGE_TAG}, resolved during + # bootstrap (module enablement) to the PR image tag below. cluster_config: e2e/tests/cluster_config.ci.yml # Create a fresh cluster through Deckhouse Commander for each run. cluster_provider: commander + # Check out the same storage-e2e ref for scripts / cmd / Go code. + storage_e2e_ref: main # Install the image built for this PR (build_dev publishes the pr tag). module_image_tag: pr${{ github.event.pull_request.number }} secrets: inherit diff --git a/e2e/README.md b/e2e/README.md index 1979907..5aed448 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -32,14 +32,15 @@ Other labels: The Commander endpoint, token and template come from inherited org/repo secrets and vars (`E2E_COMMANDER_*`); see `storage-e2e` `docs/CI.md` for the full list. -Pipeline flow (`resolve → bootstrap → enable-modules → run-tests → teardown`): -`bootstrap` creates the cluster via Commander and hands off its kubeconfig; -`enable-modules` installs `sds-object` from `tests/cluster_config.ci.yml` -(`modulePullOverride: "${E2E_MODULE_IMAGE_TAG}"`, resolved to `pr`); -`run-tests` connects to that cluster from the kubeconfig (no SSH) and runs the -suite. This requires the PR's dev image (`pr`) to be built and pushed to the -dev-registry **before** the e2e run (the `build_dev` workflow), and the Commander -cluster must be able to pull from that registry. +Pipeline flow (`resolve → bootstrap → run-tests → teardown`): `bootstrap` +creates the cluster via Commander **and** enables `sds-object` (+ its +dependencies) from `tests/cluster_config.ci.yml` +(`modulePullOverride: "${E2E_MODULE_IMAGE_TAG}"`, resolved to `pr`), +connecting in-process to the master (SSH via the bastion + API tunnel); +`run-tests` attaches to that cluster the same way (the commander provider's +`Connect`) and runs the suite. This requires the PR's dev image (`pr`) to be +built and pushed to the dev-registry **before** the e2e run (the `build_dev` +workflow), and the Commander cluster must be able to pull from that registry. ## Running locally diff --git a/e2e/go.mod b/e2e/go.mod index c8293aa..87ae236 100644 --- a/e2e/go.mod +++ b/e2e/go.mod @@ -4,16 +4,17 @@ go 1.26.0 require ( github.com/deckhouse/sds-object/api v0.0.0-00010101000000-000000000000 - github.com/deckhouse/storage-e2e v0.0.0-20260615225534-f681188c4aa9 - github.com/onsi/ginkgo/v2 v2.23.3 - github.com/onsi/gomega v1.37.0 + github.com/deckhouse/storage-e2e v0.0.0-20260702114304-4e82b7fbdf07 + github.com/onsi/ginkgo/v2 v2.28.2 + github.com/onsi/gomega v1.39.1 k8s.io/api v0.34.2 k8s.io/apimachinery v0.34.2 k8s.io/client-go v0.34.2 ) require ( - github.com/Masterminds/semver/v3 v3.3.1 // indirect + github.com/Masterminds/semver/v3 v3.5.0 // indirect + github.com/caarlos0/env/v11 v11.4.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deckhouse/deckhouse v1.74.0 // indirect github.com/deckhouse/sds-node-configurator/api v0.0.0-20260114125558-7fd7152586ff // indirect @@ -30,14 +31,14 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kr/fs v0.1.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/spdystream v0.5.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -45,26 +46,28 @@ require ( github.com/openshift/custom-resource-status v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pkg/sftp v1.13.10 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.54.0 // indirect + golang.org/x/oauth2 v0.35.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + golang.org/x/tools v0.44.0 // indirect + google.golang.org/protobuf v1.36.11 // 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 k8s.io/apiextensions-apiserver v0.34.2 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect - k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + k8s.io/utils v0.0.0-20260108192941-914a6e750570 // indirect kubevirt.io/api v1.6.2 // indirect kubevirt.io/containerized-data-importer-api v1.60.3-0.20241105012228-50fbed985de9 // indirect kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect diff --git a/e2e/go.sum b/e2e/go.sum index 8dc11f2..d2aea24 100644 --- a/e2e/go.sum +++ b/e2e/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= -github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= +github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= @@ -10,6 +10,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkY github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= 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.4.1 h1:fYwH0sWEsBSMPG7t4e/PEfTFzrWrpjyygXyUnWiSwEw= +github.com/caarlos0/env/v11 v11.4.1/go.mod h1:qupehSf/Y0TUTsxKywqRt/vJjN5nz6vauiYEUUr8P4U= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -26,8 +28,8 @@ github.com/deckhouse/deckhouse v1.74.0 h1:a/gEuLKutoV6ReWaBWMDJ+VLlOkkCwS4VMvR/s github.com/deckhouse/deckhouse v1.74.0/go.mod h1:qMuvDbP8AYghXkWmDjoFPc6r1w9uw/cWxl/hmvA0BzA= github.com/deckhouse/sds-node-configurator/api v0.0.0-20260114125558-7fd7152586ff h1:G6H7rkm/AvL6xWwbNO14gyistC3p48weL0sLCvpJnyI= github.com/deckhouse/sds-node-configurator/api v0.0.0-20260114125558-7fd7152586ff/go.mod h1:X5ftUa4MrSXMKiwQYa4lwFuGtrs+HoCNa8Zl6TPrGo8= -github.com/deckhouse/storage-e2e v0.0.0-20260615225534-f681188c4aa9 h1:wmVRE5bRWa9zaMZa8ONtbGZzC0O/khUtnAFMfvSqu74= -github.com/deckhouse/storage-e2e v0.0.0-20260615225534-f681188c4aa9/go.mod h1:doSBXYCD2liOefcARKnBQo+iGp9rUtSTZ81tX+f4vsw= +github.com/deckhouse/storage-e2e v0.0.0-20260702114304-4e82b7fbdf07 h1:AALFeS9xRX1eZ/b43Q403bLpMlUnfJMLYNn8CNLHxxs= +github.com/deckhouse/storage-e2e v0.0.0-20260702114304-4e82b7fbdf07/go.mod h1:RAdH9NafGN5bBTt14lbkO/dR6sqQwfavOApOGnUt6D4= github.com/deckhouse/virtualization/api v1.8.0 h1:wR4Ivcg56OWJRGWrZjEL+0mQrHFEG0gKn0xrq1yzjy0= github.com/deckhouse/virtualization/api v1.8.0/go.mod h1:jqKdfrs7bhU5kbn6JTJUix8N180UkugJIa3TnOTqdmA= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= @@ -48,6 +50,12 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= +github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -75,6 +83,8 @@ github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -102,8 +112,8 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83 h1:z2ogiKUYzX5Is6zr/vP9vJGqPwcdqsWjOt+V8J7+bTc= +github.com/google/pprof v0.0.0-20260115054156-294ebfa9ad83/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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= @@ -117,6 +127,8 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= +github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -137,10 +149,14 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y= +github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= 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= @@ -162,15 +178,15 @@ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108 github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= +github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= +github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= github.com/openshift/custom-resource-status v1.1.2 h1:C3DL44LEbvlbItfd8mT5jWrqPfHnSOQoQf/sypqA6A4= github.com/openshift/custom-resource-status v1.1.2/go.mod h1:DB/Mf2oTeiAmVVX1gN+NEqweonAPY0TKUwADizj8+ZA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -183,8 +199,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -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/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= @@ -193,8 +209,8 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -205,6 +221,14 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +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.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 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= @@ -223,8 +247,8 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -233,6 +257,8 @@ 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/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -251,17 +277,19 @@ golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w= +golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -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/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ= +golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -285,20 +313,20 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -314,8 +342,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210820212750-d4cc65f0b2ff/go.mod h1:YD9qOF0M9xpSpdWTBbzEl5e/RnCefISl8E5Noe10jFM= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= 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= @@ -341,8 +369,8 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -393,8 +421,8 @@ k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOP k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20211116205334-6203023598ed/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= +k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= kubevirt.io/api v1.6.2 h1:aoqZ4KsbOyDjLnuDw7H9wEgE/YTd/q5BBmYeQjJNizc= kubevirt.io/api v1.6.2/go.mod h1:p66fEy/g79x7VpgUwrkUgOoG2lYs5LQq37WM6JXMwj4= kubevirt.io/containerized-data-importer-api v1.60.3-0.20241105012228-50fbed985de9 h1:KTb8wO1Lxj220DX7d2Rdo9xovvlyWWNo3AVm2ua+1nY= diff --git a/e2e/tests/cluster_config.ci.yml b/e2e/tests/cluster_config.ci.yml index 82aedfe..eb50ae9 100644 --- a/e2e/tests/cluster_config.ci.yml +++ b/e2e/tests/cluster_config.ci.yml @@ -1,14 +1,14 @@ # CI-only cluster config for the storage-e2e reusable pipeline (commander # provider). It differs from cluster_config.yml in ONE way: the sds-object # modulePullOverride is the env reference ${E2E_MODULE_IMAGE_TAG}, resolved by -# the pipeline's enable-modules step (LoadClusterDefinition) to the PR's image -# tag. The local-run loader rejects ${...}, which is why this lives in a -# separate file and cluster_config.yml keeps a literal tag. +# the bootstrap step (LoadClusterDefinition) to the PR's image tag. The local-run +# loader rejects ${...}, which is why this lives in a separate file and +# cluster_config.yml keeps a literal tag. # # In the commander CI flow only the `modules:` list + dkpParameters.registryRepo -# are consumed (by enable-modules); the masters/workers/subnets below are present -# to satisfy validation — the cluster itself is created from the Commander -# template, not from this block. +# are consumed (module enablement runs inside bootstrap); the masters/workers/ +# subnets below are present to satisfy validation — the cluster itself is created +# from the Commander template, not from this block. clusterDefinition: masters: - hostname: "master-1" @@ -34,10 +34,47 @@ clusterDefinition: registryRepo: "dev-registry.deckhouse.io/sys/deckhouse-oss" devBranch: "main" modules: + # SeaweedFS (Full profile) keeps its filer metadata in a shared PostgreSQL + # provisioned by managed-postgres. Enabled so the Full e2e specs can run; + # harmless for the System/Lightweight (Garage) profiles. + - name: "managed-postgres" + version: 1 + enabled: true + dependencies: [] + # Heavy profile (Ceph RGW) is provisioned on top of an sds-elastic + # ElasticCluster. sds-elastic vendors Rook and needs sds-node-configurator + # (BlockDevices/LVM for OSDs) + csi-ceph (its ElasticCluster readiness + # includes a CsiCephReady stage) + snapshot-controller. modulePullOverride + # "main" pins the released layer; harmless for the other profiles. + - name: "snapshot-controller" + version: 1 + enabled: true + modulePullOverride: "main" + dependencies: [] + - name: "sds-node-configurator" + version: 1 + enabled: true + settings: + enableThinProvisioning: true + dependencies: [] + - name: "csi-ceph" + version: 1 + enabled: true + modulePullOverride: "main" + dependencies: + - "snapshot-controller" + - name: "sds-elastic" + version: 1 + enabled: true + modulePullOverride: "main" + dependencies: + - "snapshot-controller" + - "sds-node-configurator" + - "csi-ceph" - name: "sds-object" version: 1 enabled: true - # Resolved by enable-modules to the PR image tag (e.g. pr123). The + # Resolved during bootstrap to the PR image tag (e.g. pr123). The # pipeline exports E2E_MODULE_IMAGE_TAG from the module_image_tag input. modulePullOverride: "${E2E_MODULE_IMAGE_TAG}" dependencies: [] diff --git a/e2e/tests/e2e_shared_test.go b/e2e/tests/e2e_shared_test.go index dc65e60..aae1680 100644 --- a/e2e/tests/e2e_shared_test.go +++ b/e2e/tests/e2e_shared_test.go @@ -47,6 +47,7 @@ const ( envOSCType = "E2E_OSC_TYPE" envRedundancy = "E2E_REDUNDANCY" envStorageClass = "E2E_STORAGE_CLASS" + envPVCStorageClass = "E2E_PVC_STORAGE_CLASS" envOSCSize = "E2E_OSC_SIZE" envElasticRef = "E2E_ELASTIC_CLUSTER_REF" envBucketName = "E2E_BUCKET_NAME" @@ -113,13 +114,14 @@ type e2eConfig struct { // Single source of truth: TEST_CLUSTER_NAMESPACE (also the base VM namespace). namespace string - oscName string - oscType string - redundancy string - storageCl string - oscSize string - elasticRef string - bucketName string + oscName string + oscType string + redundancy string + storageCl string + pvcStorageClass string + oscSize string + elasticRef string + bucketName string oscReadyTimeout time.Duration obReadyTimeout time.Duration @@ -143,15 +145,16 @@ var ( func loadConfig() e2eConfig { cfg := e2eConfig{ - namespace: strings.TrimSpace(os.Getenv("TEST_CLUSTER_NAMESPACE")), - oscName: strings.TrimSpace(os.Getenv(envOSCName)), - oscType: strings.TrimSpace(os.Getenv(envOSCType)), - redundancy: strings.TrimSpace(os.Getenv(envRedundancy)), - storageCl: strings.TrimSpace(os.Getenv(envStorageClass)), - oscSize: strings.TrimSpace(os.Getenv(envOSCSize)), - elasticRef: strings.TrimSpace(os.Getenv(envElasticRef)), - bucketName: strings.TrimSpace(os.Getenv(envBucketName)), - probeImage: strings.TrimSpace(os.Getenv(envProbeImage)), + namespace: strings.TrimSpace(os.Getenv("TEST_CLUSTER_NAMESPACE")), + oscName: strings.TrimSpace(os.Getenv(envOSCName)), + oscType: strings.TrimSpace(os.Getenv(envOSCType)), + redundancy: strings.TrimSpace(os.Getenv(envRedundancy)), + storageCl: strings.TrimSpace(os.Getenv(envStorageClass)), + pvcStorageClass: strings.TrimSpace(os.Getenv(envPVCStorageClass)), + oscSize: strings.TrimSpace(os.Getenv(envOSCSize)), + elasticRef: strings.TrimSpace(os.Getenv(envElasticRef)), + bucketName: strings.TrimSpace(os.Getenv(envBucketName)), + probeImage: strings.TrimSpace(os.Getenv(envProbeImage)), } if cfg.namespace == "" { @@ -201,6 +204,43 @@ func (c e2eConfig) isSystem() bool { return c.oscType == string(objectv1alpha1.ClusterTypeSystem) } +// resolvePVCStorageClass picks the StorageClass for the PVC-backed profiles +// (Lightweight = Garage on PVC, Full = SeaweedFS on PVCs): E2E_PVC_STORAGE_CLASS, +// else E2E_STORAGE_CLASS, else the cluster's default StorageClass. Returns "" +// when none is available (the dependent specs then skip). +func resolvePVCStorageClass(ctx context.Context) (string, error) { + if suiteCfg.pvcStorageClass != "" { + return suiteCfg.pvcStorageClass, nil + } + if suiteCfg.storageCl != "" { + return suiteCfg.storageCl, nil + } + scs, err := suiteClientset.StorageV1().StorageClasses().List(ctx, metav1.ListOptions{}) + if err != nil { + return "", err + } + for i := range scs.Items { + if scs.Items[i].Annotations["storageclass.kubernetes.io/is-default-class"] == "true" { + return scs.Items[i].Name, nil + } + } + return "", nil +} + +// groupVersionServed reports whether the apiserver serves the given +// "group/version" (used to gate the Full specs on the managed-postgres Postgres +// CRD being present). +func groupVersionServed(gv string) (bool, error) { + _, err := suiteClientset.Discovery().ServerResourcesForGroupVersion(gv) + if err == nil { + return true, nil + } + if apierrors.IsNotFound(err) { + return false, nil + } + return false, err +} + // envBool parses a permissive boolean env value ("true"/"1"/"yes", any case). func envBool(raw string) bool { switch strings.ToLower(strings.TrimSpace(raw)) { @@ -259,6 +299,39 @@ func waitModuleReady(ctx context.Context) error { return storagekube.WaitForModuleReady(ctx, suiteRestCfg, moduleName, suiteCfg.moduleReadyTO) } +// controllerDeploymentName is the sds-object controller Deployment in the module +// namespace. Its Pod runs both the reconciler and the "webhooks" container that +// backs the validating webhooks (webhooks.d8-sds-object.svc). +const controllerDeploymentName = "controller" + +// waitControllerReady blocks until the sds-object controller Deployment has a +// Ready replica. The Deckhouse Module going Ready does not guarantee the +// controller Pod passed its readiness probe, so without this the first +// ObjectStorageCluster create can race the validating webhook and fail with +// "failed calling webhook ... connect: operation not permitted" (no ready +// endpoint behind the webhook Service yet). +func waitControllerReady(ctx context.Context, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + var last string + for { + dep, err := suiteClientset.AppsV1().Deployments(moduleNS).Get(ctx, controllerDeploymentName, metav1.GetOptions{}) + if err == nil { + if dep.Status.ReadyReplicas >= 1 && dep.Status.ReadyReplicas == dep.Status.Replicas { + return nil + } + last = fmt.Sprintf("ready=%d/%d (updated=%d)", dep.Status.ReadyReplicas, dep.Status.Replicas, dep.Status.UpdatedReplicas) + } else { + last = err.Error() + } + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for Deployment %s/%s to be Ready; last: %s", moduleNS, controllerDeploymentName, last) + } + if !sleepCtx(ctx, pollInterval) { + return ctx.Err() + } + } +} + // --- ObjectStorageCluster / ObjectBucket builders -------------------------- // buildOSC renders an ObjectStorageCluster from the suite config. storage and diff --git a/e2e/tests/full_test.go b/e2e/tests/full_test.go new file mode 100644 index 0000000..6f0f323 --- /dev/null +++ b/e2e/tests/full_test.go @@ -0,0 +1,157 @@ +/* +Copyright 2026 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 tests + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + objectv1alpha1 "github.com/deckhouse/sds-object/api/v1alpha1" +) + +// postgresGroupVersion is the managed-postgres API that the SeaweedFS (Full) +// filer stores its metadata in; the Full specs require it to be served. +const postgresGroupVersion = "managed-services.deckhouse.io/v1alpha1" + +// fullOSCReadyTimeout is generous: the Full profile brings up a distributed +// SeaweedFS (master/volume/filer) AND waits for managed-postgres to provision +// the shared filer database, which takes longer than a Garage cluster. +const fullOSCReadyTimeout = 30 * time.Minute + +// fullSpecs exercises the Full profile (distributed SeaweedFS whose filer +// metadata lives in a shared PostgreSQL from the managed-postgres module) on its +// own cluster, alongside the primary flow: create → bucket + creds Secret → S3 +// round-trip → delete. It needs a StorageClass and the managed-postgres CRD +// (enabled via cluster_config); it skips when either is missing, or when the +// primary profile is already Full. +func fullSpecs() { + Describe("full", Ordered, func() { + const oscName = "e2e-osc-full" + const bucketName = "e2e-full-bucket" + + var ( + storageClass string + secretName string + ) + + BeforeAll(func() { + if suiteCfg.oscType == string(objectv1alpha1.ClusterTypeFull) { + Skip("primary profile is already Full; dedicated Full specs would duplicate it") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + // Full requires the managed-postgres Postgres CRD (enabled via + // cluster_config's modules list). Skip if it is not served. + served, err := groupVersionServed(postgresGroupVersion) + Expect(err).NotTo(HaveOccurred(), "discover %s", postgresGroupVersion) + if !served { + Skip("managed-postgres is not installed (" + postgresGroupVersion + " not served); Full needs it for the SeaweedFS filer metadata store") + } + + storageClass, err = resolvePVCStorageClass(ctx) + Expect(err).NotTo(HaveOccurred(), "resolve StorageClass for Full") + if storageClass == "" { + Skip("no StorageClass available for Full; set E2E_PVC_STORAGE_CLASS (or E2E_STORAGE_CLASS), or mark a default StorageClass") + } + GinkgoWriter.Printf("Full profile using StorageClass %q (size %s)\n", storageClass, suiteCfg.oscSize) + }) + + It("creates a Full ObjectStorageCluster (SeaweedFS) and reaches Ready", func() { + ctx, cancel := context.WithTimeout(context.Background(), fullOSCReadyTimeout+2*time.Minute) + defer cancel() + + By("creating Full ObjectStorageCluster " + oscName) + osc := newOSC(oscName, map[string]interface{}{ + "type": string(objectv1alpha1.ClusterTypeFull), + "redundancy": string(objectv1alpha1.RedundancySingle), + "storage": map[string]interface{}{ + "size": suiteCfg.oscSize, + "class": storageClass, + }, + }) + Expect(createOSC(ctx, osc)).To(Succeed()) + + By("waiting for the cluster Ready condition (SeaweedFS + managed-postgres)") + Expect(waitCondition(ctx, objectStorageClusterGVR, "", oscName, + objectv1alpha1.OSCConditionReady, "True", fullOSCReadyTimeout)).To(Succeed()) + + backend, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "backend", "type") + Expect(err).NotTo(HaveOccurred()) + Expect(backend).To(Equal(string(objectv1alpha1.BackendSeaweedFS)), "Full is backed by SeaweedFS") + + endpoint, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "endpoint", "internal") + Expect(err).NotTo(HaveOccurred()) + Expect(endpoint).NotTo(BeEmpty()) + }) + + It("provisions a bucket and a complete credentials Secret", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.obReadyTimeout+2*time.Minute) + defer cancel() + + By("creating ObjectBucket " + bucketName) + ob := buildOB(bucketName, suiteCfg.namespace, oscName, objectv1alpha1.BucketReclaimDelete) + Expect(createOB(ctx, ob)).To(Succeed()) + + By("waiting for the bucket Ready condition") + Expect(waitOBReady(ctx, suiteCfg.namespace, bucketName)).To(Succeed()) + + var err error + secretName, err = getStringField(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, "status", "secretRef", "name") + Expect(err).NotTo(HaveOccurred()) + Expect(secretName).NotTo(BeEmpty()) + + secret, err := suiteClientset.CoreV1().Secrets(suiteCfg.namespace).Get(ctx, secretName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "get credentials Secret %s", secretName) + for _, key := range credsSecretKeys { + Expect(secret.Data).To(HaveKey(key)) + Expect(secret.Data[key]).NotTo(BeEmpty(), "credentials Secret %s must be non-empty", key) + } + }) + + It("performs an S3 write/list/read round-trip via the credentials", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.probeJobTimeout+2*time.Minute) + defer cancel() + + Expect(secretName).NotTo(BeEmpty()) + Expect(runS3ProbeJob(ctx, "s3-probe-full", suiteCfg.namespace, secretName)).To(Succeed()) + }) + + It("deletes the Full bucket and cluster", func() { + ctx, cancel := context.WithTimeout(context.Background(), resourceGoneTimeout+2*time.Minute) + defer cancel() + + By("deleting ObjectBucket " + bucketName) + Expect(suiteDyn.Resource(objectBucketGVR).Namespace(suiteCfg.namespace). + Delete(ctx, bucketName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, resourceGoneTimeout)).To(Succeed()) + if secretName != "" { + Expect(waitSecretGone(ctx, suiteCfg.namespace, secretName, 2*time.Minute)).To(Succeed()) + } + + By("deleting ObjectStorageCluster " + oscName) + Expect(suiteDyn.Resource(objectStorageClusterGVR). + Delete(ctx, oscName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectStorageClusterGVR, "", oscName, resourceGoneTimeout)).To(Succeed()) + }) + }) +} diff --git a/e2e/tests/heavy_test.go b/e2e/tests/heavy_test.go new file mode 100644 index 0000000..b5330bd --- /dev/null +++ b/e2e/tests/heavy_test.go @@ -0,0 +1,193 @@ +/* +Copyright 2026 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 tests + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + objectv1alpha1 "github.com/deckhouse/sds-object/api/v1alpha1" + "github.com/deckhouse/storage-e2e/pkg/testkit" +) + +const ( + // sdsElasticRookGroupVersion is the Rook API group sds-elastic vendors under a + // renamed group; it is served only when the sds-elastic module is installed, + // so the Heavy specs use it to detect sds-elastic and skip otherwise. (The + // ElasticCluster kind lives under storage.deckhouse.io, which sds-object also + // serves, so it cannot be used as the presence signal.) + sdsElasticRookGroupVersion = "internal.sdselastic.deckhouse.io/v1" + + // heavyECReadyTimeout covers sds-elastic bringing up the full Rook Ceph + // cluster (mon/mgr/osd + csi-ceph wiring) behind the ElasticCluster. + heavyECReadyTimeout = 30 * time.Minute + // heavyOSCReadyTimeout covers the CephObjectStore (RGW) provisioning once the + // ElasticCluster is Ready. + heavyOSCReadyTimeout = 20 * time.Minute + + // OSD selector labels applied to storage nodes and consumable BlockDevices, + // matched by the ElasticCluster storage selectors. sds-object-specific so they + // never collide with sds-elastic's own e2e labels. + heavyNodeLabelKey = "sds-object-e2e.storage.deckhouse.io/storage-node" + heavyNodeLabelVal = "true" + heavyOSDLabelKey = "sds-object-e2e.storage.deckhouse.io/osd" + heavyOSDLabelVal = "true" + // heavyMinOSDBlockDevices is the floor of consumable OSD BlockDevices that must + // surface on the storage nodes before the ElasticCluster can come up. + heavyMinOSDBlockDevices = 1 +) + +// heavySpecs exercises the Heavy profile (Ceph RADOS Gateway on top of an +// sds-elastic ElasticCluster) on its own cluster, alongside the primary flow: +// bring up the ElasticCluster (Rook Ceph) → create Heavy OSC → bucket + creds +// Secret → S3 round-trip → delete. It needs the sds-elastic module (enabled via +// cluster_config) and spare block devices for Ceph OSDs; it skips when +// sds-elastic is not installed, or when the primary profile is already Heavy. +func heavySpecs() { + Describe("heavy", Ordered, func() { + const oscName = "e2e-osc-heavy" + const bucketName = "e2e-heavy-bucket" + const ecName = "e2e-osc-heavy-ec" // cluster-scoped, <=30 chars, DNS-1123 + + var secretName string + + BeforeAll(func() { + if suiteCfg.oscType == string(objectv1alpha1.ClusterTypeHeavy) { + Skip("primary profile is already Heavy; dedicated Heavy specs would duplicate it") + } + + // sds-elastic provides the Ceph substrate; skip when it is not installed. + served, err := groupVersionServed(sdsElasticRookGroupVersion) + Expect(err).NotTo(HaveOccurred(), "discover %s", sdsElasticRookGroupVersion) + if !served { + Skip("sds-elastic is not installed (" + sdsElasticRookGroupVersion + " not served); Heavy needs it for the Ceph RGW substrate") + } + + ctx, cancel := context.WithTimeout(context.Background(), heavyECReadyTimeout+10*time.Minute) + defer cancel() + + By("labelling storage nodes and consumable OSD BlockDevices for the ElasticCluster") + _, err = testkit.EnsureElasticOSDBlockDevices(ctx, suiteRestCfg, testkit.ElasticOSDBlockDevicesConfig{ + NodeLabelKey: heavyNodeLabelKey, + NodeLabelValue: heavyNodeLabelVal, + BlockDeviceLabelKey: heavyOSDLabelKey, + BlockDeviceLabelValue: heavyOSDLabelVal, + MinBlockDevices: heavyMinOSDBlockDevices, + }) + Expect(err).NotTo(HaveOccurred(), "prepare OSD BlockDevices for the ElasticCluster") + + By("creating the ElasticCluster " + ecName + " and waiting for Ready (Rook Ceph)") + _, err = testkit.EnsureElasticCluster(ctx, suiteRestCfg, testkit.ElasticClusterConfig{ + Name: ecName, + NodeSelectorMatchLabels: map[string]string{heavyNodeLabelKey: heavyNodeLabelVal}, + BlockDeviceSelectorMatchLabels: map[string]string{heavyOSDLabelKey: heavyOSDLabelVal}, + ReadyTimeout: heavyECReadyTimeout, + }) + Expect(err).NotTo(HaveOccurred(), "ElasticCluster %s did not reach Ready", ecName) + }) + + AfterAll(func() { + // Tear the ElasticCluster down so the Ceph substrate does not linger for + // the rest of the suite / teardown. Best-effort: log but do not fail. + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + if err := testkit.TeardownElasticCluster(ctx, suiteRestCfg, ecName, 15*time.Minute); err != nil { + GinkgoWriter.Printf("warning: ElasticCluster %s teardown failed: %v\n", ecName, err) + } + }) + + It("creates a Heavy ObjectStorageCluster (Ceph RGW) and reaches Ready", func() { + ctx, cancel := context.WithTimeout(context.Background(), heavyOSCReadyTimeout+2*time.Minute) + defer cancel() + + By("creating Heavy ObjectStorageCluster " + oscName) + osc := newOSC(oscName, map[string]interface{}{ + "type": string(objectv1alpha1.ClusterTypeHeavy), + "redundancy": string(objectv1alpha1.RedundancySingle), + "elasticClusterRef": ecName, + }) + Expect(createOSC(ctx, osc)).To(Succeed()) + + By("waiting for the cluster Ready condition (Ceph RGW)") + Expect(waitCondition(ctx, objectStorageClusterGVR, "", oscName, + objectv1alpha1.OSCConditionReady, "True", heavyOSCReadyTimeout)).To(Succeed()) + + backend, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "backend", "type") + Expect(err).NotTo(HaveOccurred()) + Expect(backend).To(Equal(string(objectv1alpha1.BackendCephRGW)), "Heavy is backed by Ceph RGW") + + endpoint, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "endpoint", "internal") + Expect(err).NotTo(HaveOccurred()) + Expect(endpoint).NotTo(BeEmpty()) + }) + + It("provisions a bucket and a complete credentials Secret", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.obReadyTimeout+2*time.Minute) + defer cancel() + + By("creating ObjectBucket " + bucketName) + ob := buildOB(bucketName, suiteCfg.namespace, oscName, objectv1alpha1.BucketReclaimDelete) + Expect(createOB(ctx, ob)).To(Succeed()) + + By("waiting for the bucket Ready condition") + Expect(waitOBReady(ctx, suiteCfg.namespace, bucketName)).To(Succeed()) + + var err error + secretName, err = getStringField(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, "status", "secretRef", "name") + Expect(err).NotTo(HaveOccurred()) + Expect(secretName).NotTo(BeEmpty()) + + secret, err := suiteClientset.CoreV1().Secrets(suiteCfg.namespace).Get(ctx, secretName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "get credentials Secret %s", secretName) + for _, key := range credsSecretKeys { + Expect(secret.Data).To(HaveKey(key)) + Expect(secret.Data[key]).NotTo(BeEmpty(), "credentials Secret %s must be non-empty", key) + } + }) + + It("performs an S3 write/list/read round-trip via the credentials", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.probeJobTimeout+2*time.Minute) + defer cancel() + + Expect(secretName).NotTo(BeEmpty()) + Expect(runS3ProbeJob(ctx, "s3-probe-heavy", suiteCfg.namespace, secretName)).To(Succeed()) + }) + + It("deletes the Heavy bucket and cluster", func() { + ctx, cancel := context.WithTimeout(context.Background(), resourceGoneTimeout+2*time.Minute) + defer cancel() + + By("deleting ObjectBucket " + bucketName) + Expect(suiteDyn.Resource(objectBucketGVR).Namespace(suiteCfg.namespace). + Delete(ctx, bucketName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, resourceGoneTimeout)).To(Succeed()) + if secretName != "" { + Expect(waitSecretGone(ctx, suiteCfg.namespace, secretName, 2*time.Minute)).To(Succeed()) + } + + By("deleting ObjectStorageCluster " + oscName) + Expect(suiteDyn.Resource(objectStorageClusterGVR). + Delete(ctx, oscName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectStorageClusterGVR, "", oscName, resourceGoneTimeout)).To(Succeed()) + }) + }) +} diff --git a/e2e/tests/lightweight_test.go b/e2e/tests/lightweight_test.go new file mode 100644 index 0000000..7e328b4 --- /dev/null +++ b/e2e/tests/lightweight_test.go @@ -0,0 +1,140 @@ +/* +Copyright 2026 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 tests + +import ( + "context" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + objectv1alpha1 "github.com/deckhouse/sds-object/api/v1alpha1" +) + +// lightweightSpecs exercises the Lightweight profile (Garage backed by a PVC on +// a StorageClass) on its own cluster, alongside the primary (default: System) +// flow: create → bucket + creds Secret → S3 round-trip → delete. It needs a +// StorageClass (E2E_LIGHTWEIGHT_STORAGE_CLASS / E2E_STORAGE_CLASS / the cluster +// default); when none is available it skips. Skipped, too, when the primary +// profile is already Lightweight (the create/delete specs cover it then). +func lightweightSpecs() { + Describe("lightweight", Ordered, func() { + const oscName = "e2e-osc-light" + const bucketName = "e2e-light-bucket" + + var ( + storageClass string + secretName string + ) + + BeforeAll(func() { + if suiteCfg.oscType == string(objectv1alpha1.ClusterTypeLightweight) { + Skip("primary profile is already Lightweight; dedicated Lightweight specs would duplicate it") + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + var err error + storageClass, err = resolvePVCStorageClass(ctx) + Expect(err).NotTo(HaveOccurred(), "resolve StorageClass for Lightweight") + if storageClass == "" { + Skip("no StorageClass available for Lightweight; set E2E_PVC_STORAGE_CLASS (or E2E_STORAGE_CLASS), or mark a default StorageClass") + } + GinkgoWriter.Printf("Lightweight profile using StorageClass %q (size %s)\n", storageClass, suiteCfg.oscSize) + }) + + It("creates a Lightweight ObjectStorageCluster (Garage on PVC) and reaches Ready", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.oscReadyTimeout+2*time.Minute) + defer cancel() + + By("creating Lightweight ObjectStorageCluster " + oscName) + osc := newOSC(oscName, map[string]interface{}{ + "type": string(objectv1alpha1.ClusterTypeLightweight), + "redundancy": string(objectv1alpha1.RedundancySingle), + "storage": map[string]interface{}{ + "size": suiteCfg.oscSize, + "class": storageClass, + }, + }) + Expect(createOSC(ctx, osc)).To(Succeed()) + + By("waiting for the cluster Ready condition") + Expect(waitOSCReady(ctx, oscName)).To(Succeed()) + + backend, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "backend", "type") + Expect(err).NotTo(HaveOccurred()) + Expect(backend).To(Equal(string(objectv1alpha1.BackendGarage)), "Lightweight is backed by Garage") + + endpoint, err := getStringField(ctx, objectStorageClusterGVR, "", oscName, "status", "endpoint", "internal") + Expect(err).NotTo(HaveOccurred()) + Expect(endpoint).NotTo(BeEmpty()) + }) + + It("provisions a bucket and a complete credentials Secret", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.obReadyTimeout+2*time.Minute) + defer cancel() + + By("creating ObjectBucket " + bucketName) + ob := buildOB(bucketName, suiteCfg.namespace, oscName, objectv1alpha1.BucketReclaimDelete) + Expect(createOB(ctx, ob)).To(Succeed()) + + By("waiting for the bucket Ready condition") + Expect(waitOBReady(ctx, suiteCfg.namespace, bucketName)).To(Succeed()) + + var err error + secretName, err = getStringField(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, "status", "secretRef", "name") + Expect(err).NotTo(HaveOccurred()) + Expect(secretName).NotTo(BeEmpty()) + + secret, err := suiteClientset.CoreV1().Secrets(suiteCfg.namespace).Get(ctx, secretName, metav1.GetOptions{}) + Expect(err).NotTo(HaveOccurred(), "get credentials Secret %s", secretName) + for _, key := range credsSecretKeys { + Expect(secret.Data).To(HaveKey(key)) + Expect(secret.Data[key]).NotTo(BeEmpty(), "credentials Secret %s must be non-empty", key) + } + }) + + It("performs an S3 write/list/read round-trip via the credentials", func() { + ctx, cancel := context.WithTimeout(context.Background(), suiteCfg.probeJobTimeout+2*time.Minute) + defer cancel() + + Expect(secretName).NotTo(BeEmpty()) + Expect(runS3ProbeJob(ctx, "s3-probe-light", suiteCfg.namespace, secretName)).To(Succeed()) + }) + + It("deletes the Lightweight bucket and cluster", func() { + ctx, cancel := context.WithTimeout(context.Background(), resourceGoneTimeout+2*time.Minute) + defer cancel() + + By("deleting ObjectBucket " + bucketName) + Expect(suiteDyn.Resource(objectBucketGVR).Namespace(suiteCfg.namespace). + Delete(ctx, bucketName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectBucketGVR, suiteCfg.namespace, bucketName, resourceGoneTimeout)).To(Succeed()) + if secretName != "" { + Expect(waitSecretGone(ctx, suiteCfg.namespace, secretName, 2*time.Minute)).To(Succeed()) + } + + By("deleting ObjectStorageCluster " + oscName) + Expect(suiteDyn.Resource(objectStorageClusterGVR). + Delete(ctx, oscName, metav1.DeleteOptions{})).To(Succeed()) + Expect(waitResourceGone(ctx, objectStorageClusterGVR, "", oscName, resourceGoneTimeout)).To(Succeed()) + }) + }) +} diff --git a/e2e/tests/sds_object_suite_test.go b/e2e/tests/sds_object_suite_test.go index 741e877..4512d6b 100644 --- a/e2e/tests/sds_object_suite_test.go +++ b/e2e/tests/sds_object_suite_test.go @@ -48,7 +48,10 @@ func TestSdsObject(t *testing.T) { suiteConfig, reporterConfig := GinkgoConfiguration() if os.Getenv("CI") != "" { suiteConfig.FailFast = true - suiteConfig.Timeout = 120 * time.Minute + // Generous: the Heavy profile brings up a full Rook Ceph cluster (via an + // sds-elastic ElasticCluster) on top of the Full (SeaweedFS + Postgres) and + // Garage profiles, so the whole suite can run well past an hour. + suiteConfig.Timeout = 180 * time.Minute } // The suite shares one ObjectStorageCluster across dependency-ordered specs // (create -> bucket + S3 round-trip -> validation guards -> delete), so spec @@ -77,9 +80,12 @@ var _ = Describe("sds-object e2e", Ordered, ContinueOnFailure, func() { dumpFailedSpecDiagnostics(ctx) }) - createSpecs() // create_test.go: OSC -> Ready, OB -> Ready, creds Secret, S3 round-trip - validationSpecs() // validation_test.go: webhook + CEL admission guards - deleteSpecs() // delete_test.go: OB delete (+ creds Secret + reclaim), OSC delete + createSpecs() // create_test.go: OSC -> Ready, OB -> Ready, creds Secret, S3 round-trip + validationSpecs() // validation_test.go: webhook + CEL admission guards + lightweightSpecs() // lightweight_test.go: Lightweight (Garage on PVC) create -> bucket -> round-trip -> delete + fullSpecs() // full_test.go: Full (SeaweedFS + managed-postgres) create -> bucket -> round-trip -> delete + heavySpecs() // heavy_test.go: Heavy (Ceph RGW on sds-elastic ElasticCluster) bring-up -> create -> bucket -> round-trip -> delete + deleteSpecs() // delete_test.go: OB delete (+ creds Secret + reclaim), OSC delete }) func prepareSuite() { @@ -126,6 +132,9 @@ func prepareSuite() { By("Waiting for the sds-object module to become Ready") Expect(waitModuleReady(ctx)).To(Succeed(), "sds-object module readiness") + By("Waiting for the sds-object controller (validating webhook) to be Ready") + Expect(waitControllerReady(ctx, suiteCfg.moduleReadyTO)).To(Succeed(), "sds-object controller/webhook readiness") + By("Ensuring the in-cluster test namespace exists") Expect(ensureNamespace(ctx, suiteCfg.namespace)).To(Succeed()) } diff --git a/images/controller/internal/backend/garage/buckets.go b/images/controller/internal/backend/garage/buckets.go index ce20095..9d0e421 100644 --- a/images/controller/internal/backend/garage/buckets.go +++ b/images/controller/internal/backend/garage/buckets.go @@ -26,6 +26,7 @@ import ( v1alpha1 "github.com/deckhouse/sds-object/api/v1alpha1" "github.com/deckhouse/sds-object/images/controller/internal/backend" + "github.com/deckhouse/sds-object/images/controller/internal/backend/s3util" ) // EnsureBucket creates the bucket and an access key scoped to it, and reports @@ -109,18 +110,28 @@ func (d *Driver) DeleteBucket(ctx context.Context, cluster *v1alpha1.ObjectStora } svc := newAdminClient(adminEndpoint(cluster, d.namespace, d.clusterDomain), token) - accessKeyID, _, err := d.existingCreds(ctx, bucket) + accessKeyID, secretKey, err := d.existingCreds(ctx, bucket) if err != nil { return err } - if accessKeyID != "" { - if err := svc.deleteKey(ctx, accessKeyID); err != nil { - return fmt.Errorf("delete access key: %w", err) - } - } if bucket.Spec.ReclaimPolicy == v1alpha1.BucketReclaimDelete { name := bucketDisplayName(bucket) + + // Garage's admin DELETE /v1/bucket refuses a non-empty bucket + // (409 BucketNotEmpty), so empty it over S3 first. This uses the + // bucket's own credentials and therefore must run before the access + // key is deleted below. + if accessKeyID != "" && secretKey != "" { + mc, cerr := s3util.NewClient(s3HostPort(cluster, d.namespace, d.clusterDomain), accessKeyID, secretKey) + if cerr != nil { + return fmt.Errorf("build S3 client to empty bucket %q: %w", name, cerr) + } + if eerr := s3util.EmptyBucket(ctx, mc, name); eerr != nil { + return fmt.Errorf("empty bucket %q before delete: %w", name, eerr) + } + } + b, found, err := svc.getBucketByAlias(ctx, name) if err != nil { return fmt.Errorf("look up bucket %q: %w", name, err) @@ -131,6 +142,13 @@ func (d *Driver) DeleteBucket(ctx context.Context, cluster *v1alpha1.ObjectStora } } } + + // Delete the access key last: emptying the bucket above needs it. + if accessKeyID != "" { + if err := svc.deleteKey(ctx, accessKeyID); err != nil { + return fmt.Errorf("delete access key: %w", err) + } + } return nil } diff --git a/images/controller/internal/backend/garage/resources.go b/images/controller/internal/backend/garage/resources.go index a44a6b3..e03e0b3 100644 --- a/images/controller/internal/backend/garage/resources.go +++ b/images/controller/internal/backend/garage/resources.go @@ -76,6 +76,12 @@ func s3Endpoint(cluster *v1alpha1.ObjectStorageCluster, namespace, clusterDomain return fmt.Sprintf("http://%s.%s.svc.%s:%d", s3SvcName(cluster), namespace, clusterDomain, s3Port) } +// s3HostPort is the in-cluster S3 endpoint as host:port (no scheme), for the +// minio/S3 client used to empty buckets before deletion. +func s3HostPort(cluster *v1alpha1.ObjectStorageCluster, namespace, clusterDomain string) string { + return fmt.Sprintf("%s.%s.svc.%s:%d", s3SvcName(cluster), namespace, clusterDomain, s3Port) +} + // adminEndpoint is the in-cluster admin API URL of the cluster's Service. func adminEndpoint(cluster *v1alpha1.ObjectStorageCluster, namespace, clusterDomain string) string { return fmt.Sprintf("http://%s.%s.svc.%s:%d", s3SvcName(cluster), namespace, clusterDomain, adminPort) diff --git a/images/controller/internal/backend/s3util/s3util.go b/images/controller/internal/backend/s3util/s3util.go index 9f7a015..bf19df7 100644 --- a/images/controller/internal/backend/s3util/s3util.go +++ b/images/controller/internal/backend/s3util/s3util.go @@ -54,8 +54,10 @@ func EnsureBucket(ctx context.Context, mc *minio.Client, name, region string) er return nil } -// DeleteBucket empties and removes the bucket (best effort). -func DeleteBucket(ctx context.Context, mc *minio.Client, name string) error { +// EmptyBucket removes all objects from the bucket (best effort, idempotent). No +// error if the bucket does not exist. Some backends (Garage) refuse to delete a +// non-empty bucket, so callers empty it over S3 before removing it. +func EmptyBucket(ctx context.Context, mc *minio.Client, name string) error { exists, err := mc.BucketExists(ctx, name) if err != nil { return fmt.Errorf("check bucket %q: %w", name, err) @@ -70,6 +72,22 @@ func DeleteBucket(ctx context.Context, mc *minio.Client, name string) error { return fmt.Errorf("empty bucket %q: %w", name, rerr.Err) } } + return nil +} + +// DeleteBucket empties and removes the bucket (best effort). +func DeleteBucket(ctx context.Context, mc *minio.Client, name string) error { + exists, err := mc.BucketExists(ctx, name) + if err != nil { + return fmt.Errorf("check bucket %q: %w", name, err) + } + if !exists { + return nil + } + + if err := EmptyBucket(ctx, mc, name); err != nil { + return err + } if err := mc.RemoveBucket(ctx, name); err != nil { return fmt.Errorf("remove bucket %q: %w", name, err) diff --git a/images/controller/internal/backend/seaweedfs/postgres.go b/images/controller/internal/backend/seaweedfs/postgres.go index bb68205..35cefb7 100644 --- a/images/controller/internal/backend/seaweedfs/postgres.go +++ b/images/controller/internal/backend/seaweedfs/postgres.go @@ -19,6 +19,9 @@ package seaweedfs import ( "context" "fmt" + "reflect" + "sort" + "strings" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -53,6 +56,16 @@ func pgName(cluster *v1alpha1.ObjectStorageCluster) string { return componentName(cluster, "pg") } +// pgHost is the managed-postgres read-write Service the filer connects to. +// managed-postgres names the backing CNPG cluster "d8ms-pg-" and exposes a +// "-rw" Service, so this name is deterministic. The driver relies on it +// when the credentials Secret momentarily drops the "host" key — which +// managed-postgres does whenever the Postgres instance is briefly not serving +// (startup, failover), keeping only the stable username/password. +func pgHost(cluster *v1alpha1.ObjectStorageCluster) string { + return "d8ms-pg-" + pgName(cluster) + "-rw" +} + // pgCredsSecretName is the Secret managed-postgres writes the filer user's // credentials into (keys: host, username, password). The driver sets it via the // user's storeCredsToSecret. @@ -120,6 +133,12 @@ func buildPostgres(cluster *v1alpha1.ObjectStorageCluster, namespace string) *un // managed-postgres rw endpoint enables TLS, so sslmode=require (encrypt without // CA verification, matching managed-postgres' own DSN default). func renderFilerToml(host string, port int, user, password, database string) string { + // createTable is REQUIRED for the postgres2 store: SeaweedFS runs + // fmt.Sprintf(createTable, ) for each metadata table (filemeta, one + // per bucket). Without it the store formats an empty template and sends + // `%!(EXTRA string=filemeta)` to Postgres — "syntax error at or near %!". + // The "%%s" escape keeps a literal "%s" in the rendered TOML for SeaweedFS to + // fill; the leading verbs below bind host/port/user/password/database in order. return fmt.Sprintf(`[postgres2] enabled = true hostname = "%s" @@ -128,6 +147,15 @@ username = "%s" password = "%s" database = "%s" sslmode = "require" +createTable = """ +CREATE TABLE IF NOT EXISTS "%%s" ( + dirhash BIGINT, + name VARCHAR(65535), + directory VARCHAR(65535), + meta bytea, + PRIMARY KEY (dirhash, name) +); +""" `, host, port, user, password, database) } @@ -169,12 +197,89 @@ func (d *Driver) ensurePostgres(ctx context.Context, cluster *v1alpha1.ObjectSto return err } - existing.Object["spec"] = desired.Object["spec"] - existing.SetLabels(desired.GetLabels()) + // Overlay only the fields we manage and update only when something actually + // changed. Replacing the whole spec unconditionally (the previous behaviour) + // dropped any defaults managed-postgres writes back, so every reconcile bumped + // the Postgres generation and kept managed-postgres perpetually re-syncing. + before := existing.DeepCopy() + + // managed-postgres generates the user's password on first reconcile, writes + // the plaintext to storeCredsToSecret and stores the resulting hash back into + // spec.users[].hashedPassword (deleting the plaintext). Our desired users omit + // it, so overlaying would strip hashedPassword — which makes managed-postgres + // regenerate the password, desyncing the DB role from the credentials the + // filer already read ("password authentication failed"). Carry the + // managed-postgres-owned password fields over so the overlay preserves them. + preservePgUserSecrets(desired, existing) + + spec, _, _ := unstructured.NestedMap(existing.Object, "spec") + if spec == nil { + spec = map[string]interface{}{} + } + for k, v := range desired.Object["spec"].(map[string]interface{}) { + spec[k] = v + } + existing.Object["spec"] = spec + + labels := existing.GetLabels() + if labels == nil { + labels = map[string]string{} + } + for k, v := range desired.GetLabels() { + labels[k] = v + } + existing.SetLabels(labels) existing.SetOwnerReferences(desired.GetOwnerReferences()) + + if reflect.DeepEqual(before.Object, existing.Object) { + return nil + } return d.client.Update(ctx, existing) } +// preservePgUserSecrets copies the managed-postgres-owned password fields +// (hashedPassword / password) from the existing Postgres CR into the matching +// desired users (by name), so the overlay in ensurePostgres does not strip them +// and trigger a password regeneration. +func preservePgUserSecrets(desired, existing *unstructured.Unstructured) { + desiredUsers, _, _ := unstructured.NestedSlice(desired.Object, "spec", "users") + existingUsers, _, _ := unstructured.NestedSlice(existing.Object, "spec", "users") + if len(desiredUsers) == 0 || len(existingUsers) == 0 { + return + } + + existingByName := make(map[string]map[string]interface{}, len(existingUsers)) + for _, u := range existingUsers { + if m, ok := u.(map[string]interface{}); ok { + if name, _ := m["name"].(string); name != "" { + existingByName[name] = m + } + } + } + + changed := false + for _, u := range desiredUsers { + m, ok := u.(map[string]interface{}) + if !ok { + continue + } + name, _ := m["name"].(string) + ex, ok := existingByName[name] + if !ok { + continue + } + for _, key := range []string{"hashedPassword", "password"} { + if v, ok := ex[key]; ok { + m[key] = v + changed = true + } + } + } + if changed { + _ = unstructured.SetNestedSlice(desired.Object, desiredUsers, "spec", "users") + } +} + // pgCreds reads the filer DB credentials managed-postgres writes into the // storeCredsToSecret Secret. Returns ok=false (not an error) while the Secret // or its keys are not yet populated, so the caller can requeue. @@ -183,16 +288,33 @@ func (d *Driver) pgCreds(ctx context.Context, cluster *v1alpha1.ObjectStorageClu key := client.ObjectKey{Namespace: d.namespace, Name: pgCredsSecretName(cluster)} if err := d.apiReader.Get(ctx, key, secret); err != nil { if apierrors.IsNotFound(err) { + d.log.Info(fmt.Sprintf("[seaweedfs] pg creds Secret %s not found yet, waiting", key)) return "", "", "", false, nil } return "", "", "", false, err } - host = string(secret.Data["host"]) + // Only username/password are required and stable. managed-postgres withdraws + // "host" (and the dsn) from the Secret whenever the Postgres instance is not + // serving, so depending on it would stall SeaweedFS for the whole startup; + // instead fall back to the deterministic read-write Service and let the filer + // retry the DB connection until Postgres is ready. user = string(secret.Data["username"]) password = string(secret.Data["password"]) - if host == "" || user == "" || password == "" { + if user == "" || password == "" { + keys := make([]string, 0, len(secret.Data)) + for k := range secret.Data { + keys = append(keys, k) + } + sort.Strings(keys) + d.log.Info(fmt.Sprintf("[seaweedfs] pg creds Secret %s present but incomplete: keys=[%s] userEmpty=%t passEmpty=%t", + key, strings.Join(keys, ","), user == "", password == "")) return "", "", "", false, nil } + host = string(secret.Data["host"]) + if host == "" { + host = pgHost(cluster) + d.log.Info(fmt.Sprintf("[seaweedfs] pg creds Secret %s has no host yet (Postgres not serving); using derived Service %q", key, host)) + } return host, user, password, true, nil } diff --git a/images/controller/internal/backend/seaweedfs/resources_test.go b/images/controller/internal/backend/seaweedfs/resources_test.go index 51e2b36..a402e79 100644 --- a/images/controller/internal/backend/seaweedfs/resources_test.go +++ b/images/controller/internal/backend/seaweedfs/resources_test.go @@ -195,11 +195,20 @@ func TestRenderFilerToml(t *testing.T) { `password = "s3cr3t"`, `database = "seaweedfs"`, `sslmode = "require"`, + // createTable is required by the postgres2 store and must keep a LITERAL + // %s placeholder for SeaweedFS to fill with each table name at runtime. + "createTable =", + `CREATE TABLE IF NOT EXISTS "%s"`, } { if !strings.Contains(toml, want) { t.Errorf("filer.toml missing %q:\n%s", want, toml) } } + // A stray %! means our Sprintf consumed SeaweedFS's %s placeholder (the exact + // bug that made the filer send `%!(EXTRA ...)` to Postgres). + if strings.Contains(toml, "%!") { + t.Errorf("filer.toml contains a botched format verb:\n%s", toml) + } } func TestBuildFilerConfigSecret(t *testing.T) {