From a3de7197d349f8dcfa12859be8f7e160dc1813a6 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Fri, 20 Feb 2026 10:07:59 +0300 Subject: [PATCH 01/26] feat: scaffold project base components Signed-off-by: Ilya Drey --- .dmtlint.yaml | 31 + .github/workflows/build.yaml | 44 + .github/workflows/deploy.yaml | 45 + .gitignore | 40 + .helmignore | 12 + .werf/consts.yaml | 21 + .werf/defines/image-build.tmpl | 20 + .werf/defines/image-mountpoints.tmpl | 32 + .werf/defines/images.tmpl | 49 + .werf/defines/packages-clean.tmpl | 12 + .werf/defines/packages-proxies.tmpl | 70 + .werf/defines/parse-base-images-map.tmpl | 41 + .werf/images.yaml | 56 + CODE_OF_CONDUCT.md | 132 + CONTRIBUTING.md | 166 + Chart.yaml | 6 + LICENSE | 214 + MAINTAINERS.md | 6 + SECURITY.md | 7 + Taskfile.yaml | 113 + build/base-images/deckhouse_images.yml | 306 ++ build/components/README.md | 14 + build/components/versions.yml | 4 + charts/deckhouse_lib_helm-1.55.1.tgz | Bin 0 -> 26935 bytes crds/embedded/helm-controller.yaml | 2631 +++++++++++ crds/embedded/nelm-source-controller.yaml | 4102 +++++++++++++++++ docs/CONFIGURATION.md | 4 + docs/CONFIGURATION_RU.md | 4 + docs/CR.md | 4 + docs/CR_RU.md | 4 + docs/README.md | 16 + docs/README_RU.md | 16 + docs/images/.keep | 0 docs/internal/components_placement.md | 3 + hooks/.keep | 0 images/helm-controller/werf.inc.yaml | 3 + images/kube-api-rewriter/.dockerignore | 9 + images/kube-api-rewriter/.gitignore | 1 + images/kube-api-rewriter/METRICS.md | 166 + images/kube-api-rewriter/STRUCTURE.md | 451 ++ images/kube-api-rewriter/Taskfile.dist.yaml | 118 + .../cmd/kube-api-rewriter/main.go | 224 + images/kube-api-rewriter/go.mod | 76 + images/kube-api-rewriter/go.sum | 216 + images/kube-api-rewriter/local/Dockerfile | 45 + .../local/kube-api-rewriter.kubeconfig | 11 + .../local/proxy-gen-certs.sh | 93 + .../local/proxy-kubeconfig-cm.yaml | 20 + images/kube-api-rewriter/local/proxy.yaml | 158 + .../local/test-controller/go.mod | 90 + .../local/test-controller/go.sum | 484 ++ .../local/test-controller/main.go | 369 ++ images/kube-api-rewriter/mount-points.yaml | 7 + .../pkg/kubevirt/kubevirt_rules.go | 698 +++ .../pkg/kubevirt/kubevirt_rules_test.go | 33 + .../pkg/labels/context_values.go | 104 + images/kube-api-rewriter/pkg/log/attrs.go | 31 + images/kube-api-rewriter/pkg/log/body.go | 83 + images/kube-api-rewriter/pkg/log/differ.go | 133 + .../pkg/log/pretty_handler.go | 248 + .../pkg/log/pretty_handler_test.go | 72 + images/kube-api-rewriter/pkg/log/setup.go | 120 + .../pkg/monitoring/healthz/handler.go | 35 + .../pkg/monitoring/metrics/handler.go | 34 + .../pkg/monitoring/metrics/registry.go | 40 + .../pkg/monitoring/profiler/handler.go | 35 + .../pkg/proxy/bytes_counter.go | 76 + images/kube-api-rewriter/pkg/proxy/doc.go | 55 + images/kube-api-rewriter/pkg/proxy/handler.go | 551 +++ .../pkg/proxy/handler_test.go | 778 ++++ images/kube-api-rewriter/pkg/proxy/logger.go | 35 + images/kube-api-rewriter/pkg/proxy/metrics.go | 126 + .../pkg/proxy/metrics_provider.go | 276 ++ .../pkg/proxy/stream_handler.go | 311 ++ .../pkg/rewriter/3rdparty.go | 32 + .../pkg/rewriter/admission_configuration.go | 89 + .../rewriter/admission_configuration_test.go | 85 + .../pkg/rewriter/admission_policy.go | 67 + .../pkg/rewriter/admission_review.go | 238 + .../pkg/rewriter/admission_review_test.go | 225 + .../pkg/rewriter/affinity.go | 187 + .../pkg/rewriter/api_endpoint.go | 313 ++ .../pkg/rewriter/api_endpoint_test.go | 292 ++ images/kube-api-rewriter/pkg/rewriter/app.go | 91 + .../pkg/rewriter/app_test.go | 253 + images/kube-api-rewriter/pkg/rewriter/core.go | 87 + .../pkg/rewriter/core_test.go | 379 ++ images/kube-api-rewriter/pkg/rewriter/crd.go | 257 ++ .../pkg/rewriter/crd_test.go | 336 ++ .../pkg/rewriter/discovery.go | 574 +++ .../pkg/rewriter/discovery_test.go | 606 +++ .../kube-api-rewriter/pkg/rewriter/events.go | 52 + .../pkg/rewriter/events_test.go | 123 + images/kube-api-rewriter/pkg/rewriter/gvk.go | 69 + .../pkg/rewriter/indexer/map_indexer.go | 58 + images/kube-api-rewriter/pkg/rewriter/list.go | 101 + images/kube-api-rewriter/pkg/rewriter/load.go | 38 + images/kube-api-rewriter/pkg/rewriter/map.go | 39 + .../pkg/rewriter/metadata.go | 144 + images/kube-api-rewriter/pkg/rewriter/path.go | 191 + .../kube-api-rewriter/pkg/rewriter/policy.go | 28 + .../pkg/rewriter/prefixed_name_rewriter.go | 288 ++ images/kube-api-rewriter/pkg/rewriter/rbac.go | 159 + .../pkg/rewriter/rbac_test.go | 184 + .../pkg/rewriter/resource.go | 161 + .../pkg/rewriter/resource_test.go | 383 ++ .../pkg/rewriter/rule_rewriter.go | 426 ++ .../pkg/rewriter/rule_rewriter_test.go | 418 ++ .../kube-api-rewriter/pkg/rewriter/rules.go | 405 ++ .../pkg/rewriter/rules_test.go | 119 + .../pkg/rewriter/target_request.go | 306 ++ .../pkg/rewriter/transformers.go | 111 + .../kube-api-rewriter/pkg/rewriter/webhook.go | 17 + .../pkg/server/http_server.go | 158 + .../pkg/server/runnable_group.go | 90 + .../pkg/target/kubernetes.go | 55 + .../kube-api-rewriter/pkg/target/webhook.go | 106 + .../pkg/tls/certmanager/certmanager.go | 27 + .../filesystem/file-cert-manager.go | 170 + images/kube-api-rewriter/pkg/tls/util/util.go | 52 + images/kube-api-rewriter/werf.inc.yaml | 64 + images/nelm-source-controller/werf.inc.yaml | 3 + module.yaml | 25 + openapi/config-values.yaml | 26 + openapi/doc-ru-config-values.yaml | 24 + openapi/values.yaml | 20 + oss.yaml | 12 + requirements.lock | 6 + templates/_helpers.tpl | 25 + templates/helm-controller/_helpers.tpl | 6 + templates/helm-controller/deployment.yaml | 137 + templates/helm-controller/rbac-for-us.yaml | 148 + .../helm-controller/service-metrics.yaml | 15 + .../helm-controller/service-monitor.yaml | 23 + .../_customize_patch_helpers.tpl | 69 + templates/kube-api-rewriter/_settings.tpl | 32 + .../kube-api-rewriter/_sidecar_helpers.tpl | 199 + .../cm-kubeconfig-local.yaml | 20 + templates/kube-rbac-proxy/_helpers.tpl | 92 + templates/namespace.yaml | 8 + templates/nelm-source-controller/_helpers.tpl | 8 + .../nelm-source-controller/deployment.yaml | 143 + .../nelm-source-controller/rbac-for-us.yaml | 169 + .../service-metrics.yaml | 15 + .../service-monitor.yaml | 23 + templates/nelm-source-controller/service.yaml | 15 + templates/rbac-to-us.yaml | 32 + templates/registry-secret.yaml | 16 + tmp/mc-operator-helm.yaml | 8 + tmp/modulepulloverride.yaml | 8 + tmp/modulesource.yaml | 8 + tools/validation/diff.go | 149 + tools/validation/doc_changes.go | 143 + tools/validation/go.mod | 3 + tools/validation/main.go | 97 + tools/validation/messages.go | 176 + tools/validation/no_cyrillic.go | 160 + tools/validation/no_cyrillic_test.go | 96 + werf-giterminism.yaml | 28 + werf.yaml | 114 + werf_cleanup.yaml | 18 + 161 files changed, 25612 insertions(+) create mode 100644 .dmtlint.yaml create mode 100644 .github/workflows/build.yaml create mode 100644 .github/workflows/deploy.yaml create mode 100644 .gitignore create mode 100644 .helmignore create mode 100644 .werf/consts.yaml create mode 100644 .werf/defines/image-build.tmpl create mode 100644 .werf/defines/image-mountpoints.tmpl create mode 100644 .werf/defines/images.tmpl create mode 100644 .werf/defines/packages-clean.tmpl create mode 100644 .werf/defines/packages-proxies.tmpl create mode 100644 .werf/defines/parse-base-images-map.tmpl create mode 100644 .werf/images.yaml create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 Chart.yaml create mode 100644 LICENSE create mode 100644 MAINTAINERS.md create mode 100644 SECURITY.md create mode 100644 Taskfile.yaml create mode 100644 build/base-images/deckhouse_images.yml create mode 100644 build/components/README.md create mode 100644 build/components/versions.yml create mode 100644 charts/deckhouse_lib_helm-1.55.1.tgz create mode 100644 crds/embedded/helm-controller.yaml create mode 100644 crds/embedded/nelm-source-controller.yaml create mode 100644 docs/CONFIGURATION.md create mode 100644 docs/CONFIGURATION_RU.md create mode 100644 docs/CR.md create mode 100644 docs/CR_RU.md create mode 100644 docs/README.md create mode 100644 docs/README_RU.md create mode 100644 docs/images/.keep create mode 100644 docs/internal/components_placement.md create mode 100644 hooks/.keep create mode 100644 images/helm-controller/werf.inc.yaml create mode 100644 images/kube-api-rewriter/.dockerignore create mode 100644 images/kube-api-rewriter/.gitignore create mode 100644 images/kube-api-rewriter/METRICS.md create mode 100644 images/kube-api-rewriter/STRUCTURE.md create mode 100644 images/kube-api-rewriter/Taskfile.dist.yaml create mode 100644 images/kube-api-rewriter/cmd/kube-api-rewriter/main.go create mode 100644 images/kube-api-rewriter/go.mod create mode 100644 images/kube-api-rewriter/go.sum create mode 100644 images/kube-api-rewriter/local/Dockerfile create mode 100644 images/kube-api-rewriter/local/kube-api-rewriter.kubeconfig create mode 100755 images/kube-api-rewriter/local/proxy-gen-certs.sh create mode 100644 images/kube-api-rewriter/local/proxy-kubeconfig-cm.yaml create mode 100644 images/kube-api-rewriter/local/proxy.yaml create mode 100644 images/kube-api-rewriter/local/test-controller/go.mod create mode 100644 images/kube-api-rewriter/local/test-controller/go.sum create mode 100644 images/kube-api-rewriter/local/test-controller/main.go create mode 100644 images/kube-api-rewriter/mount-points.yaml create mode 100644 images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go create mode 100644 images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go create mode 100644 images/kube-api-rewriter/pkg/labels/context_values.go create mode 100644 images/kube-api-rewriter/pkg/log/attrs.go create mode 100644 images/kube-api-rewriter/pkg/log/body.go create mode 100644 images/kube-api-rewriter/pkg/log/differ.go create mode 100644 images/kube-api-rewriter/pkg/log/pretty_handler.go create mode 100644 images/kube-api-rewriter/pkg/log/pretty_handler_test.go create mode 100644 images/kube-api-rewriter/pkg/log/setup.go create mode 100644 images/kube-api-rewriter/pkg/monitoring/healthz/handler.go create mode 100644 images/kube-api-rewriter/pkg/monitoring/metrics/handler.go create mode 100644 images/kube-api-rewriter/pkg/monitoring/metrics/registry.go create mode 100644 images/kube-api-rewriter/pkg/monitoring/profiler/handler.go create mode 100644 images/kube-api-rewriter/pkg/proxy/bytes_counter.go create mode 100644 images/kube-api-rewriter/pkg/proxy/doc.go create mode 100644 images/kube-api-rewriter/pkg/proxy/handler.go create mode 100644 images/kube-api-rewriter/pkg/proxy/handler_test.go create mode 100644 images/kube-api-rewriter/pkg/proxy/logger.go create mode 100644 images/kube-api-rewriter/pkg/proxy/metrics.go create mode 100644 images/kube-api-rewriter/pkg/proxy/metrics_provider.go create mode 100644 images/kube-api-rewriter/pkg/proxy/stream_handler.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/3rdparty.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/admission_configuration.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/admission_configuration_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/admission_policy.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/admission_review.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/admission_review_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/affinity.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/api_endpoint.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/api_endpoint_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/app.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/app_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/core.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/core_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/crd.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/crd_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/discovery.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/discovery_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/events.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/events_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/gvk.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/indexer/map_indexer.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/list.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/load.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/map.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/metadata.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/path.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/policy.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/prefixed_name_rewriter.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rbac.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rbac_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/resource.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/resource_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rule_rewriter_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rules.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/rules_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/target_request.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/transformers.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/webhook.go create mode 100644 images/kube-api-rewriter/pkg/server/http_server.go create mode 100644 images/kube-api-rewriter/pkg/server/runnable_group.go create mode 100644 images/kube-api-rewriter/pkg/target/kubernetes.go create mode 100644 images/kube-api-rewriter/pkg/target/webhook.go create mode 100644 images/kube-api-rewriter/pkg/tls/certmanager/certmanager.go create mode 100644 images/kube-api-rewriter/pkg/tls/certmanager/filesystem/file-cert-manager.go create mode 100644 images/kube-api-rewriter/pkg/tls/util/util.go create mode 100644 images/kube-api-rewriter/werf.inc.yaml create mode 100644 images/nelm-source-controller/werf.inc.yaml create mode 100644 module.yaml create mode 100644 openapi/config-values.yaml create mode 100644 openapi/doc-ru-config-values.yaml create mode 100644 openapi/values.yaml create mode 100644 oss.yaml create mode 100644 requirements.lock create mode 100644 templates/_helpers.tpl create mode 100644 templates/helm-controller/_helpers.tpl create mode 100644 templates/helm-controller/deployment.yaml create mode 100644 templates/helm-controller/rbac-for-us.yaml create mode 100644 templates/helm-controller/service-metrics.yaml create mode 100644 templates/helm-controller/service-monitor.yaml create mode 100644 templates/kube-api-rewriter/_customize_patch_helpers.tpl create mode 100644 templates/kube-api-rewriter/_settings.tpl create mode 100644 templates/kube-api-rewriter/_sidecar_helpers.tpl create mode 100644 templates/kube-api-rewriter/cm-kubeconfig-local.yaml create mode 100644 templates/kube-rbac-proxy/_helpers.tpl create mode 100644 templates/namespace.yaml create mode 100644 templates/nelm-source-controller/_helpers.tpl create mode 100644 templates/nelm-source-controller/deployment.yaml create mode 100644 templates/nelm-source-controller/rbac-for-us.yaml create mode 100644 templates/nelm-source-controller/service-metrics.yaml create mode 100644 templates/nelm-source-controller/service-monitor.yaml create mode 100644 templates/nelm-source-controller/service.yaml create mode 100644 templates/rbac-to-us.yaml create mode 100644 templates/registry-secret.yaml create mode 100644 tmp/mc-operator-helm.yaml create mode 100644 tmp/modulepulloverride.yaml create mode 100644 tmp/modulesource.yaml create mode 100644 tools/validation/diff.go create mode 100644 tools/validation/doc_changes.go create mode 100644 tools/validation/go.mod create mode 100644 tools/validation/main.go create mode 100644 tools/validation/messages.go create mode 100644 tools/validation/no_cyrillic.go create mode 100644 tools/validation/no_cyrillic_test.go create mode 100644 werf-giterminism.yaml create mode 100644 werf.yaml create mode 100644 werf_cleanup.yaml diff --git a/.dmtlint.yaml b/.dmtlint.yaml new file mode 100644 index 0000000..7c2aab5 --- /dev/null +++ b/.dmtlint.yaml @@ -0,0 +1,31 @@ +global: + linters-settings: + documentation: + impact: error +linters-settings: + openapi: + exclude-rules: + enum: + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.sts.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.sts.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy.properties" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.uninstall.properties.deletionPropagation" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.driftDetection.properties.mode" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy" + - "spec.versions[0].schema.openAPIV3Schema.properties.spec.properties.chart.properties.spec.properties.verify.properties.provider" + - "spec.versions[0].schema.openAPIV3Schema.properties.status.properties.lastAttemptedReleaseAction" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.chart.properties.spec.properties.verify.properties.provider" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.driftDetection.properties.mode" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.postRenderers.items.properties.kustomize.properties.patchesJson6902.items.properties.patch.items.properties.op" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.uninstall.properties.deletionPropagation" + - "spec.versions[1].schema.openAPIV3Schema.properties.spec.properties.upgrade.properties.remediation.properties.strategy" + - "spec.versions[1].schema.openAPIV3Schema.properties.status.properties.lastAttemptedReleaseAction" + - "properties.logLevel" + - "properties.logFormat" + rbac: + exclude-rules: + wildcards: + - kind: ClusterRole + name: d8:operator-helm:helm-controller diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..9c8e0f2 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,44 @@ +name: Build + +on: [push] + +env: + CI_COMMIT_REF_NAME: ${{ github.ref_name }} + +jobs: + lint: + runs-on: ubuntu-latest + continue-on-error: true + name: Lint + steps: + - uses: actions/checkout@v4 + - uses: deckhouse/modules-actions/lint@main + # TODO: change after MVP + # env: + # DMT_METRICS_URL: ${{ secrets.DMT_METRICS_URL }} + # DMT_METRICS_TOKEN: ${{ secrets.DMT_METRICS_TOKEN }} + + build: + runs-on: ubuntu-latest + name: Build and Push images + steps: + - uses: actions/checkout@v4 + + - uses: deckhouse/modules-actions/setup@main + with: + registry: ghcr.io + registry_login: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get the repository name + id: repo_name + run: echo "REPO_NAME=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_OUTPUT + + - uses: deckhouse/modules-actions/build@main + with: + # TODO: change after MVP + # module_source: ghcr.io/${{ github.repository_owner }}/modules + module_source: ghcr.io/deckhouse/${{ steps.repo_name.outputs.REPO_NAME }} + module_name: ${{ steps.repo_name.outputs.REPO_NAME }} + module_tag: ${{ github.ref_name }} + svace_enabled: false diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..b7919d5 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,45 @@ +name: Deploy + +on: + workflow_dispatch: + inputs: + release_channel: + description: "Select the release channel" + type: choice + default: alpha + options: + - "alpha" + - "beta" + - "early-access" + - "stable" + - "rock-solid" + tag: + description: "Tag of the module, e.g., v1.21.1" + type: string + required: true + +jobs: + deploy: + runs-on: ubuntu-latest + name: Deploy the module + steps: + - uses: actions/checkout@v4 + + - uses: deckhouse/modules-actions/setup@main + with: + registry: ghcr.io + registry_login: ${{ github.actor }} + registry_password: ${{ secrets.GITHUB_TOKEN }} + + - name: Get the repository name + id: repo_name + run: echo "REPO_NAME=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_OUTPUT + + - uses: deckhouse/modules-actions/deploy@main + with: + # TODO: change after MVP + # module_source: ghcr.io/${{ github.actor }}/modules + module_source: ghcr.io/deckhouse/${{ steps.repo_name.outputs.REPO_NAME }} + module_name: ${{ steps.repo_name.outputs.REPO_NAME }} + module_tag: ${{ github.event.inputs.tag }} + release_channel: ${{ github.event.inputs.release_channel }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2e7f2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# vim +*.swp + +# IDE +.project +.settings +.idea/ +.vscode +venv/ + +# macOS Finder files +*.DS_Store +._* + +# Python +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ + +#werf +/base_images.yml + +# opencode +**/.opencode/ diff --git a/.helmignore b/.helmignore new file mode 100644 index 0000000..4deeb35 --- /dev/null +++ b/.helmignore @@ -0,0 +1,12 @@ +crds +docs +enabled +hooks +images +lib +Makefile +openapi +*.md +release.yaml +werf*.yaml +NOTES.txt diff --git a/.werf/consts.yaml b/.werf/consts.yaml new file mode 100644 index 0000000..36403e0 --- /dev/null +++ b/.werf/consts.yaml @@ -0,0 +1,21 @@ +# Edition module settings +{{- $_ := set . "MODULE_EDITION" (env "MODULE_EDITION" "EE") }} + +# Component versions +{{- $_ := set . "Package" dict -}} +{{- $_ := set . "Core" dict -}} +{{- $versions_path := "/build/components/versions.yml" -}} + +{{- if .ModuleDir -}} +{{- $versions_path = (printf "%s%s" (trimPrefix "/" .ModuleDir ) $versions_path) -}} +{{- end -}} + +{{- $versions_ctx := (.Files.Get $versions_path | fromYaml) -}} + +{{- range $k, $v := $versions_ctx.package -}} +{{- $_ := set $.Package $k $v -}} +{{- end -}} + +{{- range $k, $v := $versions_ctx.core -}} +{{- $_ := set $.Core $k $v -}} +{{- end -}} diff --git a/.werf/defines/image-build.tmpl b/.werf/defines/image-build.tmpl new file mode 100644 index 0000000..bc7afe2 --- /dev/null +++ b/.werf/defines/image-build.tmpl @@ -0,0 +1,20 @@ +{{- define "image-build.build" }} +{{- if ne $.SVACE_ENABLED "false" }} +svace build --init --clear-build-dir {{ .BuildCommand }} +attempt=0 +retries=5 +success=0 +set +e +while [[ $attempt -lt $retries ]]; do + ssh -o ConnectTimeout=10 -o ServerAliveInterval=10 -o ServerAliveCountMax=12 {{ $.SVACE_ANALYZE_SSH_USER }}@{{ $.SVACE_ANALYZE_HOST }} mkdir -p /svace-analyze/{{ $.Commit.Hash }}/{{ $.ProjectName }}/.svace-dir + rsync -zr --timeout=10 --compress-choice=zstd --partial --append-verify .svace-dir {{ $.SVACE_ANALYZE_SSH_USER }}@{{ $.SVACE_ANALYZE_HOST }}:/svace-analyze/{{ $.Commit.Hash }}/{{ $.ProjectName }}/ && success=1 && break + sleep 10 + attempt=$((attempt + 1)) +done +set -e +[[ $success == 1 ]] && rm -rf .svace-dir || exit 1 +{{ .BuildCommand }} +{{- else }} +{{ .BuildCommand }} +{{- end }} +{{- end }} diff --git a/.werf/defines/image-mountpoints.tmpl b/.werf/defines/image-mountpoints.tmpl new file mode 100644 index 0000000..9c76a3f --- /dev/null +++ b/.werf/defines/image-mountpoints.tmpl @@ -0,0 +1,32 @@ +{{/* + +Template to bake mount points in the image. These static mount points +are required so containerd can start a container with image integrity check. + +Problem: each directory specified in volumeMounts items should exist +in image, containerd is unable to create mount point for us when +integrity check is enabled. + +Solution: define all possible mount points in mount-points.yaml file and +include this template in git section of the werf.inc.yaml. + +*/}} +{{/* NOTE: Keep in sync with version in Deckhouse CSE */}} +{{- define "image mount points" }} +{{- $mountPoints := ($.Files.Get (printf "images/%s/mount-points.yaml" $.ImageName) | fromYaml) }} +{{- $context := . }} +{{- range $v := $mountPoints.dirs }} +- add: /tools/mounts/mountdir + to: {{ $v | trimSuffix "/" }} + stageDependencies: + install: + - "**/*" +{{- end }} +{{- range $v := $mountPoints.files }} +- add: /tools/mounts/mountfile + to: {{ $v }} + stageDependencies: + install: + - "**/*" +{{- end }} +{{- end }} diff --git a/.werf/defines/images.tmpl b/.werf/defines/images.tmpl new file mode 100644 index 0000000..51152c5 --- /dev/null +++ b/.werf/defines/images.tmpl @@ -0,0 +1,49 @@ +{{/* +Template for ease of use of multiple image imports +Default stage "install". +Important! To render properly in "embedded module" mode, ensure that caller passes context with "ModuleNamePrefix" variable. + +Usage: +{{- $images := list "swtpm" "numactl" "libfuse3" -}} +{{- include "importPackageImages" (list . $images "install") -}} # install stage (default) +Result: +... + - image: packages/binaries/libfuse3 + add: /libfuse3 + to: /libfuse3 + before: install +... + +{{- include "importPackageImages" (list . $images "setup") -}} # setup stage +Result: +... + - image: packages/binaries/libfuse3 + add: /libfuse3 + to: /libfuse3 + before: setup +... +*/}} + +{{ define "importPackageImages" }} +{{- if not (eq (kindOf .) "slice") }} +{{- fail "importPackageImages: invalid type of argument, slice is expected" }} +{{- end }} +{{- $context := index . 0 }} +{{- $ImageNameList := index . 1 }} +{{- $stage := "install" }} +{{- if gt (len .) 2 }} +{{- $stage = index . 2 }} +{{- end }} +{{- range $imageName := $ImageNameList }} +{{- $packages := splitList " " $imageName -}} +{{- range $packages -}} +{{- $image := trim . -}} +{{- if ne $image "" }} +- image: {{ $context.ModuleNamePrefix }}packages/{{ $image }} + add: /{{ $image }} + to: /{{ $image }} + before: {{ $stage }} +{{- end }} +{{- end -}} +{{- end }} +{{ end }} diff --git a/.werf/defines/packages-clean.tmpl b/.werf/defines/packages-clean.tmpl new file mode 100644 index 0000000..0e77725 --- /dev/null +++ b/.werf/defines/packages-clean.tmpl @@ -0,0 +1,12 @@ +{{- define "alt packages clean" }} +- apt-get clean +- rm --recursive --force /var/lib/apt/lists/ftp.altlinux.org* /var/cache/apt/*.bin + {{- if $.DistroPackagesProxy }} +- rm --recursive --force /var/lib/apt/lists/{{ $.DistroPackagesProxy }}* + {{- end }} +{{- end }} + +{{- define "debian packages clean" }} +- apt-get clean +- find /var/lib/apt/ /var/cache/apt/ -type f -delete +{{- end }} diff --git a/.werf/defines/packages-proxies.tmpl b/.werf/defines/packages-proxies.tmpl new file mode 100644 index 0000000..e93f9d2 --- /dev/null +++ b/.werf/defines/packages-proxies.tmpl @@ -0,0 +1,70 @@ +{{- define "alt packages proxy" }} +# Replace altlinux repos with our proxy + {{- if $.DistroPackagesProxy }} +- sed -i "s|ftp.altlinux.org/pub/distributions/archive|{{ $.DistroPackagesProxy }}/repository/archive-ALT-Linux-APT-Repository|g" /etc/apt/sources.list.d/alt.list + {{- end }} +# TODO: remove this when http becomes available +# change scheme from http to ftp +- sed -i "s|rpm \[p11\] http://|#rpm [p11] http://|g" /etc/apt/sources.list.d/alt.list +- sed -i "s|#rpm \[p11\] ftp://|rpm [p11] ftp://|g" /etc/apt/sources.list.d/alt.list +- export DEBIAN_FRONTEND=noninteractive +- apt-get update -y +{{- end }} + +{{- define "alt dist upgrade" }} +- apt-get dist-upgrade -y +- find /var/cache/apt/ -type f -delete +- rm -rf /var/log/*log /var/log/apt/* /var/lib/dpkg/*-old /var/cache/debconf/*-old +{{- end }} + +{{- define "debian packages proxy" }} +# 5 years 157680000 +- | + echo "Acquire::Check-Valid-Until false;" >> /etc/apt/apt.conf + echo "Acquire::Check-Date false;" >> /etc/apt/apt.conf + echo "Acquire::Max-FutureTime 157680000;" >> /etc/apt/apt.conf +# Replace debian repos with our proxy + {{- if $.DistroPackagesProxy }} +- if [ -f /etc/apt/sources.list ]; then sed -i "s|http://deb.debian.org|http://{{ $.DistroPackagesProxy }}/repository|g" /etc/apt/sources.list; fi +- if [ -f /etc/apt/sources.list.d/debian.sources ]; then sed -i "s|http://deb.debian.org|http://{{ $.DistroPackagesProxy }}/repository|g" /etc/apt/sources.list.d/debian.sources; fi + {{- end }} +- export DEBIAN_FRONTEND=noninteractive +- apt-get update +{{- end }} + +{{- define "ubuntu packages proxy" }} + # Replace ubuntu repos with our proxy + {{- if $.DistroPackagesProxy }} +- sed -i 's|http://archive.ubuntu.com|http://{{ $.DistroPackagesProxy }}/repository/archive-ubuntu|g' /etc/apt/sources.list +- sed -i 's|http://security.ubuntu.com|http://{{ $.DistroPackagesProxy }}/repository/security-ubuntu|g' /etc/apt/sources.list + {{- end }} +- export DEBIAN_FRONTEND=noninteractive +# one year +- apt-get -o Acquire::Check-Valid-Until=false -o Acquire::Check-Date=false -o Acquire::Max-FutureTime=31536000 update +{{- end }} + +{{- define "alpine packages proxy" }} +# Replace alpine repos with our proxy + {{- if $.DistroPackagesProxy }} +- sed -i 's|https://dl-cdn.alpinelinux.org|http://{{ $.DistroPackagesProxy }}/repository|g' /etc/apk/repositories + {{- end }} +- apk update +{{- end }} + +{{- define "node packages proxy" }} + {{- if $.DistroPackagesProxy }} +- npm config set registry http://{{ $.DistroPackagesProxy }}/repository/npmjs/ + {{- end }} +{{- end }} + +{{- define "pypi proxy" }} + {{- if $.DistroPackagesProxy }} +- | + cat <<"EOD" > /etc/pip.conf + [global] + index = http://{{ $.DistroPackagesProxy }}/repository/pypi-proxy/pypi + index-url = http://{{ $.DistroPackagesProxy }}/repository/pypi-proxy/simple + trusted-host = {{ $.DistroPackagesProxy }} + EOD + {{- end }} +{{- end }} diff --git a/.werf/defines/parse-base-images-map.tmpl b/.werf/defines/parse-base-images-map.tmpl new file mode 100644 index 0000000..0a6d8b1 --- /dev/null +++ b/.werf/defines/parse-base-images-map.tmpl @@ -0,0 +1,41 @@ +{{- define "project_images"}} +{{- $globImages := "images/*/werf.inc.yaml" }} +{{- $globPackages := "images/packages/*/werf.inc.yaml" }} +{{- $globRootWerf := "werf.yaml" }} +{{- $regexp := "(builder|tools|libs|base)/([a-zA-Z0-9._-]+)" }} +{{- $globAll := merge (.Files.Glob $globImages) (.Files.Glob $globPackages) (.Files.Glob $globRootWerf) }} +{{- $imagesMap := dict }} +{{- range $path, $content := $globAll }} +{{- $findImg := regexFindAll $regexp $content -1 }} +{{- range $findImg }} +{{- $_ := set $imagesMap . "" }} +{{- end }} +{{- end }} +{{- $imagesMap | toJson }} +{{- end }} + +{{- define "parse_base_images_map" }} +{{- $deckhouseImages := .Files.Get "build/base-images/deckhouse_images.yml" | fromYaml }} +{{/* + # deckhouse_images has a format + # /: "sha256:abcde12345 +*/}} +{{- $usedImagesDict := (include "project_images" . | fromJson) }} +{{- range $k, $v := $deckhouseImages }} +{{- $baseImagePath := (printf "%s@%s" $deckhouseImages.REGISTRY_PATH (trimSuffix "/" $v)) }} +{{- if ne $k "REGISTRY_PATH" }} +{{- $_ := set $deckhouseImages $k $baseImagePath }} +{{- end }} +{{- end }} +{{- $_ := unset $deckhouseImages "REGISTRY_PATH" }} +{{- $_ := set . "Images" (mustMerge $deckhouseImages) }} +{{/* # base images artifacts */}} +{{- range $k, $v := .Images }} +{{- if hasKey $usedImagesDict $k }} +--- +image: {{ $k }} +from: {{ $v }} +final: false +{{- end }} +{{- end }} +{{- end }} diff --git a/.werf/images.yaml b/.werf/images.yaml new file mode 100644 index 0000000..61c7b53 --- /dev/null +++ b/.werf/images.yaml @@ -0,0 +1,56 @@ +{{/* # Common dirs */}} +{{- define "module_image_template" }} + {{- if eq .ImageInstructionType "Dockerfile" }} +--- +image: images/{{ .ImageName }} +context: images/{{ .ImageName }} +dockerfile: Dockerfile + {{- else }} + {{- tpl .ImageBuildData . }} + {{- end }} +{{- end }} + + +{{/* # Context inside folder images */}} +{{- $Root := . }} + +{{ $ImagesBuildFiles := .Files.Glob "images/*/{Dockerfile,werf.inc.yaml}" }} + +{{- range $path, $content := $ImagesBuildFiles }} + +{{- $ctx := dict }} +{{- $_ := set $ctx "ImageInstructionType" "Stapel" }} + +{{- $ImageData := regexReplaceAll "^images/([0-9a-z-_]+)/(Dockerfile|werf.inc.yaml)$" $path "${1}#${2}" | split "#" }} + +{{- $_ := set $ctx "ImageName" $ImageData._0 }} +{{- $_ := set $ctx "ModuleDir" "" }} +{{- $_ := set $ctx "ModuleNamePrefix" "" }} +{{- $_ := set $ctx "ImageBuildData" $content }} +{{- $_ := set $ctx "Files" $Root.Files }} +{{- $_ := set $ctx "SOURCE_REPO" $Root.SOURCE_REPO }} +{{- $_ := set $ctx "SOURCE_REPO_GIT" $Root.SOURCE_REPO_GIT }} +{{- $_ := set $ctx "MODULE_EDITION" $Root.MODULE_EDITION }} +{{- $_ := set $ctx "DEBUG_COMPONENT" $Root.DEBUG_COMPONENT }} +{{- $_ := set $ctx "Package" $Root.Package }} +{{- $_ := set $ctx "Core" $Root.Core }} +{{- $_ := set $ctx "GOPROXY" (env "GOPROXY" "https://proxy.golang.org,direct") }} +{{- $_ := set $ctx "ProjectName" $ctx.ImageName }} +{{- $_ := set $ctx "Commit" $Root.Commit }} +{{- $_ := set $ctx "SVACE_ENABLED" $Root.SVACE_ENABLED }} +{{- $_ := set $ctx "SVACE_ANALYZE_SSH_USER" $Root.SVACE_ANALYZE_SSH_USER }} +{{- $_ := set $ctx "SVACE_ANALYZE_HOST" $Root.SVACE_ANALYZE_HOST }} +{{- $_ := set $ctx "SVACE_IMAGE_SUFFIX" $Root.SVACE_IMAGE_SUFFIX }} + +{{- include "module_image_template" $ctx }} + +{{- range $ImageYamlMainfest := regexSplit "\n?---[ \t]*\n" (include "module_image_template" $ctx) -1 }} +{{- $ImageManifest := $ImageYamlMainfest | fromYaml }} +{{- if $ImageManifest | dig "final" true }} +{{- if $ImageManifest.image }} +{{- $_ := set $ "ImagesIDList" (append $.ImagesIDList $ImageManifest.image) }} +{{- end }} +{{- end }} +{{- end }} + +{{- end }} diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3bed631 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +community@deckhouse.io. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available +at [https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7c2fa4b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,166 @@ +# Contributing + +## Feedback + +The first thing we recommend is to check the existing [issues](https://github.com/deckhouse/operator-helm/issues) — there may already be a discussion or solution on your topic. If not, choose the appropriate way to address the issue on [the new issue form](https://github.com/deckhouse/operator-helm/issues/new/choose). + +## Code contributions + +1. Prepare an environment. To build and run common workflows locally, you'll need to _at least_ have the following installed: + + - [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) + - [Go](https://golang.org/doc/install) + - [Docker](https://docs.docker.com/get-docker/) + - [go-task](https://taskfile.dev/installation/) (task runner) + - [ginkgo](https://onsi.github.io/ginkgo/#installing-ginkgo) (testing framework required to run tests) + +2. [Fork the project](https://github.com/deckhouse/operator-helm/fork). + +3. Clone the project: + + ```shell + git clone https://github.com/[GITHUB_USERNAME]/operator-helm + ``` + +4. Create branch following the [branch name convention](#branch-name): + + ```shell + git checkout -b feat/core/add-new-feature + ``` + +5. Make changes. + +6. Commit changes: + + - Follow [the commit message convention](#commit-message). + - Sign off every commit you contributed as an acknowledgment of the [DCO](https://developercertificate.org/). + +7. Push commits. + +8. Create a pull request following the [pull request name convention](#pull-request-name). + +## Images + +The module images are located in the ./images directory. + +Images, such as build images or images with binary artifacts, should not be included in the module. To do so, they must be labeled as follows in the `werf.inc.yaml` file: `final: false`. + +## Conventions + +### Commit message + + + +**Examples:** + + + +#### Type + +Must be one of the following: + +* **feat**: new features or capabilities that enhance the user's experience. +* **fix**: bug fixes that enhance the user's experience. +* **refactor**: a code changes that neither fixes a bug nor adds a feature. +* **docs**: updates or improvements to documentation. +* **test**: additions or corrections to tests. +* **chore**: updates that don't fit into other types. + +#### Scope + +Scope indicates the area of the project affected by the changes. The scope can consist of a top-level scope, which broadly categorizes the changes, and can optionally include nested scopes that provide further detail. + +Supported scopes are the following: + + + +#### Subject + +The subject contains a succinct description of the change: + + - use the imperative, present tense: "change" not "changed" nor "changes" + - don't capitalize the first letter + - no dot (.) at the end + +#### Body + +Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes". +The body should include the motivation for the change and contrast this with previous behavior. + +### Branch name + +Each branch name consists of a [**type**](#type), [**scope**](#scope), and a [**short-description**](#short-description): + +``` +// +``` + +When naming branches, only the top-level scope should be used. Multiple or nested scopes are not allowed in branch names, ensuring that each branch is clearly associated with a broad area of the project. + +**Examples:** + + + +### Changes Block + +When submitting a pull request, include a **changes block** to document modifications for the changelog. This block helps automate the release changelog creation, tracks updates, and prepares release notes. + +#### Format + +The changes block consists of YAML documents, each detailing a specific change. Use the following structure: + +```` +```changes +section: +type: +summary: +impact_level: # Optional +impact: | + +``` +```` + +#### Fields Description + + - **section**: (Required) Specifies the affected scope of the project. Should be in kebab-case, choose one of [available scopes](#scope). If PR affects multiple scopes, add change block for each scope. + - Examples: `api`, `core`, `ci` + + - **type**: (Required) Defines the nature of the change: + - `feature`: Adds new functionality. + - `fix`: Resolves user-facing issues. + - `chore`: Maintenance tasks without direct user impact. + - `docs`: Changes to documentation. + + - **summary**: (Required) A concise explanation of the change, ending with a period. + + - **impact_level**: (Optional) Indicates the significance of the change. + - `high`: Requires an **impact** description and will be included in "Know before update" sections. + - `low`: Minor changes, omitted from user-facing changelogs. If this level is specified, all other fields are not validated by GitHub workflow. + + - **impact**: (Required if `impact_level` is high) Describes the change's effects, such as expected restarts or downtime. + - Examples: + - "Ingress controller will restart." + - "Expect slow downtime due to kube-apiserver restarts." + +#### Example + + + +For full guidelines, refer to [here](https://github.com/deckhouse/deckhouse/wiki/Guidelines-for-working-with-PRs). + +#### Short description + +A concise, hyphen-separated phrase in kebab-case that clearly describes the main focus of the branch. + +### Pull request name + +Each pull request title should clearly reflect the changes introduced, adhering to [**the header format** of a commit message](#commit-message), typically mirroring the main commit's text in the PR. + +**Examples** + + + +## Coding + + - [Effective Go](https://golang.org/doc/effective_go.html). + - [Go's commenting conventions](http://blog.golang.org/godoc-documenting-go-code). diff --git a/Chart.yaml b/Chart.yaml new file mode 100644 index 0000000..102d074 --- /dev/null +++ b/Chart.yaml @@ -0,0 +1,6 @@ +name: operator-helm +version: 0.0.1 +dependencies: + - name: deckhouse_lib_helm + version: 1.55.1 + repository: https://deckhouse.github.io/lib-helm diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f24a346 --- /dev/null +++ b/LICENSE @@ -0,0 +1,214 @@ +Copyright (c) 2023 Flant JSC + +Portions of this software are licensed as follows: + +* All content residing under the "docs/" directory of this repository + is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All client-side JavaScript (when served directly or after being compiled, + arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into this software are licensed under + the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above + is available under the "Apache License 2.0." license as defined below. + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 0000000..c80979e --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,6 @@ +# Core maintainers + +| Name | Email | GitHub | +| ---------------- | -------------------------- | ------------------------------------------------------- | +| Ilya Lesikov | ilya.lesikov@flant.com | [@ilya-lesikov](https://github.com/ilya-lesikov) | +| Aleksei Igrychev | aleksei.igrychev@flant.com | [@alexey-igrychev](https://github.com/alexey-igrychev) | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..35c80b2 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security + +Thank you for your concern regarding the security issues in Deckhouse project. + +Please submit any discovered vulnerabilities to security@deckhouse.io and wait for our reply within 48 hours. + +If we confirm an issue, a relevant private discussion will be created with you as its participant. Otherwise, we will reply to you, probably asking for clarifications needed to verify the security risk. diff --git a/Taskfile.yaml b/Taskfile.yaml new file mode 100644 index 0000000..2b534e9 --- /dev/null +++ b/Taskfile.yaml @@ -0,0 +1,113 @@ +version: "3" + +silent: true + +vars: + deckhouse_lib_helm_ver: 1.55.1 + target: "" + VALIDATION_FILES: "tools/validation/{main,messages,diff,no_cyrillic,doc_changes}.go" + +tasks: + check-werf: + cmds: + - which werf >/dev/null || (echo "werf not found."; exit 1) + silent: true + + check-yq: + cmds: + - which yq >/dev/null || (echo "yq not found."; exit 1) + silent: true + + check-jq: + cmds: + - which jq >/dev/null || (echo "jq not found."; exit 1) + silent: true + + check-helm: + cmds: + - which helm >/dev/null || (echo "helm not found."; exit 1) + silent: true + + helm-update-subcharts: + deps: + - check-helm + cmds: + - helm repo add deckhouse https://deckhouse.github.io/lib-helm + - helm repo update deckhouse + - helm dep update + + helm-bump-helm-lib: + deps: + - check-yq + cmds: + - yq -i '.dependencies[] |= select(.name == "deckhouse_lib_helm").version = "{{ .deckhouse_lib_helm_ver }}"' Chart.yaml + - task: helm-update-subcharts + + build: + deps: + - check-werf + cmds: + - werf build {{ .target }} + + dev:format:yaml: + desc: "Format non-templated YAML files, e.g. CRDs" + cmds: + # TODO: update image referecne + - | + docker run --rm \ + -v ./:/tmp/operator-helm ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-helm ; prettier -w \"**/*.yaml\" \"**/*.yml\"" + + dev:addlicense: + desc: |- + Add Flant CE license to files sh,go,py. Default directory is root of project, custom directory path can be passed like: "task dev:addlicense -- " + cmds: + - | + {{if .CLI_ARGS}} + go run tools/addlicense/{main,variables,msg,utils}.go -directory {{ .CLI_ARGS }} + {{else}} + go run tools/addlicense/{main,variables,msg,utils}.go -directory ./ + {{end}} + + lint: + cmds: + - task: lint:doc-ru + - task: lint:prettier:yaml + - task: virtualization-controller:dvcr:lint + - task: virtualization-controller:lint + + lint:doc-ru: + desc: "Check the correspondence between description fields in the original crd and the Russian language version" + cmds: + - | + docker run \ + --rm -it -v "$PWD:/src" docker.io/fl64/d8-doc-ru-linter:v0.0.1-dev0 \ + sh -c \ + 'for crd in /src/crds/*.yaml; do [[ "$(basename "$crd")" =~ ^doc-ru ]] || (echo ${crd}; /d8-doc-ru-linter -s "$crd" -d "/src/crds/doc-ru-$(basename "$crd")" -n /dev/null); done' + + lint:prettier:yaml: + desc: "Check if yaml files are prettier-formatted." + cmds: + # TODO: update image referecne + - | + docker run --rm \ + -v ./:/tmp/operator-nelm ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-nelm ; prettier -c \"**/*.yaml\" \"**/*.yml\"" + + validation:no-cyrillic: + desc: "No cyrillic" + cmds: + - go run {{ .VALIDATION_FILES }} --type no-cyrillic + + validation:doc-changes: + desc: "Doc-changes" + cmds: + - go run {{ .VALIDATION_FILES }} --type doc-changes + + # TODO: implement for operator-helm + # validation:helm-templates: + # desc: "Check Helm templates" + # cmds: + # - | + # cd tools/kubeconform + # ./kubeconform.sh diff --git a/build/base-images/deckhouse_images.yml b/build/base-images/deckhouse_images.yml new file mode 100644 index 0000000..c1552d1 --- /dev/null +++ b/build/base-images/deckhouse_images.yml @@ -0,0 +1,306 @@ +# version=v0.5.51 +# REGISTRY_PATH is a special key which is concatenated with other base images +REGISTRY_PATH: registry.deckhouse.io/base_images +base/distroless: "sha256:d44b8fadcf21012913301335e3bd8e96dfd6e10bdc4317e295c3d770f1c0ac2a" # from: builder/scratch +base/nginx-release-1.28.0: "sha256:678a8eefe5fe82bdc71766fe1dd3152aa4fbc54b426c2dbe31619576d72380a5" # from: tools/nginx-release-1.28.0 +base/nginx: "sha256:678a8eefe5fe82bdc71766fe1dd3152aa4fbc54b426c2dbe31619576d72380a5" # from: tools/nginx-release-1.28.0 +base/python: "sha256:48f3f53a7f26b17370a9cf835b95982e2b7fde1073b0241b9f5fc72c1faac6dc" # from: builder/scratch +base/python-v3.12.12: "sha256:48f3f53a7f26b17370a9cf835b95982e2b7fde1073b0241b9f5fc72c1faac6dc" # from: builder/scratch +base/redis-7.4.5: "sha256:3bd70725e8c0919cb925cd8b471c91493b34782ce8eaa8b6871da35bce3bdc1d" # from: builder/scratch +base/redis: "sha256:3bd70725e8c0919cb925cd8b471c91493b34782ce8eaa8b6871da35bce3bdc1d" # from: builder/scratch +base/ruby-bundler: "sha256:e6a80bd606afb83a258b939160b2c5fe1350003822384d8e3f5b80bde424dda3" # from: builder/alpine +base/ruby-bundler-v3_4_7: "sha256:e6a80bd606afb83a258b939160b2c5fe1350003822384d8e3f5b80bde424dda3" # from: builder/alpine +base/ruby: "sha256:11aa322ba4ef4eced1612684c53f5ee4a195afddcbc283bfa7be7173990dc985" # from: base/distroless +base/ruby-v3_4_7: "sha256:11aa322ba4ef4eced1612684c53f5ee4a195afddcbc283bfa7be7173990dc985" # from: base/distroless +base/scratch: "sha256:cfac1c6b53f9365ee59ffe94c90211253f2a85ece372a6a9dfad61f4e0ab0bea" # from: builder/scratch +base/shell-operator: "sha256:f3556750cf8bcbf42b09d52b23fb7e1ae98ebabe273dfe90ce415b1d6b3fa4db" # from: builder/scratch +base/shell-operator-v1.9.3: "sha256:f3556750cf8bcbf42b09d52b23fb7e1ae98ebabe273dfe90ce415b1d6b3fa4db" # from: builder/scratch +builder/alpine-3.21: "sha256:e54195b7221b3977b2abe6daaf5fabc91add7aa0a3360cd5af590c5472bb3aa7" # from: alpine:3.21.5 +builder/alpine-3.22: "sha256:2ffd38fd342a64e79ed28cf52d71216bf119b28d96b9871184acfea672a7acc8" # from: alpine:3.22.2 +builder/alpine: "sha256:2ffd38fd342a64e79ed28cf52d71216bf119b28d96b9871184acfea672a7acc8" # from: alpine:3.22.2 +builder/alpine-svace-3.21: "sha256:f72e40e3ac586391367f0f276de72b054f29c8aa95147c09f5ee39f29cfbf4ab" # from: builder/alpine-3.21 +builder/alpine-svace-3.22: "sha256:592ad64f03152d8055700374c7726450de44a2394993419d83dd4fa4a7603e34" # from: builder/alpine-3.22 +builder/alpine-svace: "sha256:592ad64f03152d8055700374c7726450de44a2394993419d83dd4fa4a7603e34" # from: builder/alpine-3.22 +builder/alt-2025-11-02: "sha256:f1b131ab710bd9ea9654a4efb9873cfda1955400ed85fe0de7d7f99416beb0f7" # from: registry.altlinux.org/p11/alt:20250625 +builder/alt: "sha256:f1b131ab710bd9ea9654a4efb9873cfda1955400ed85fe0de7d7f99416beb0f7" # from: registry.altlinux.org/p11/alt:20250625 +builder/debian-12.11-slim: "sha256:194f18cb5cdc8c4cceb4f796003b956173d63acffb1d81470b83f713a8947882" # from: debian:12.11-slim +builder/debian: "sha256:261d9b2d8d2f1b5a5d1565bd88d950e14722481b1ffc92553f1e8a2326ee7363" # from: debian:trixie-slim +builder/debian-svace-12.11-slim: "sha256:20109cd79726d2e5e7418e8a75c6cf796467d860de2d9ab57ad1af1b5841e962" # from: builder/debian-12.11-slim +builder/debian-svace: "sha256:3eaf3813bbfd39c44885f2f18c3d05cdd42aa5c67c6c88a64a3db01487ee8305" # from: builder/debian-trixie-slim +builder/debian-svace-trixie-slim: "sha256:3eaf3813bbfd39c44885f2f18c3d05cdd42aa5c67c6c88a64a3db01487ee8305" # from: builder/debian-trixie-slim +builder/debian-trixie-slim: "sha256:261d9b2d8d2f1b5a5d1565bd88d950e14722481b1ffc92553f1e8a2326ee7363" # from: debian:trixie-slim +builder/golang-alpine-1.24: "sha256:5d7346ae5dfd1f5c6e78dd285d9656b49b7926272bb93942ba6470d9d1d279f8" # from: builder/alpine +builder/golang-alpine-1.25: "sha256:425f33f84bc87800267bb5bdfb96e3a356dd9c0b6046f7fe787cdf339c7215be" # from: builder/alpine +builder/golang-alpine: "sha256:425f33f84bc87800267bb5bdfb96e3a356dd9c0b6046f7fe787cdf339c7215be" # from: builder/alpine +builder/golang-alpine-svace-1.24: "sha256:cd46caf412999b7f3ee4d1ed29a0ef4cead6423fe4056222e80393592155b6e2" # from: builder/golang-alpine-1.24 +builder/golang-alpine-svace-1.25: "sha256:7bce2f3691208bd5174a35918151ead0a85bd1a0f07fbf97842be6582586309a" # from: builder/golang-alpine-1.25 +builder/golang-alpine-svace: "sha256:7bce2f3691208bd5174a35918151ead0a85bd1a0f07fbf97842be6582586309a" # from: builder/golang-alpine-1.25 +builder/golang-alt-1.24: "sha256:c936cfce282d666be6392e78b0008cb10dcaf0d6e299f5261f0f6cd25badcd06" # from: builder/alt +builder/golang-alt-1.25: "sha256:e1f79798b704ba104eb84a38d1aa1f584cba8805313285efd40ca34a0d754291" # from: builder/alt +builder/golang-alt: "sha256:e1f79798b704ba104eb84a38d1aa1f584cba8805313285efd40ca34a0d754291" # from: builder/alt +builder/golang-alt-svace-1.24: "sha256:05d378e91966137a676f6fcb78a6b8ae0db90ffd770695d4512b2aa6824318ae" # from: builder/golang-alt-1.24 +builder/golang-alt-svace-1.25: "sha256:d7bf46f38bfddedb41f60a9ff59d3659723396c665ffcb41648c6081021c073f" # from: builder/golang-alt-1.25 +builder/golang-alt-svace: "sha256:d7bf46f38bfddedb41f60a9ff59d3659723396c665ffcb41648c6081021c073f" # from: builder/golang-alt-1.25 +builder/golang-bookworm-1.24: "sha256:bd7cdf28c1923fa71ffd8828e684e62c8f716d2e95b962ba3219beb400a3a1d8" # from: golang:1.24.13-bookworm +builder/golang-bookworm-1.25: "sha256:6ae07a7d16a540e1dd56d5a7afbdaaf450894c74c6f1b4a497b212d61c023838" # from: golang:1.25.7-bookworm +builder/golang-bookworm: "sha256:6ae07a7d16a540e1dd56d5a7afbdaaf450894c74c6f1b4a497b212d61c023838" # from: golang:1.25.7-bookworm +builder/golang-bookworm-svace-1.24: "sha256:64d5178bf902a50df988958b0cc9cf00bb43663e128ac0a9826fcbbbb99aa8f8" # from: builder/golang-bookworm-1.24 +builder/golang-bookworm-svace-1.25: "sha256:df13a2341fea0ce53b5244bb69b76862dba03472b5a71953b3e0bb51269e72b6" # from: builder/golang-bookworm-1.25 +builder/golang-bookworm-svace: "sha256:df13a2341fea0ce53b5244bb69b76862dba03472b5a71953b3e0bb51269e72b6" # from: builder/golang-bookworm-1.25 +builder/golang-bullseye-1.24: "sha256:e694827623f7a8bf932c3b9462546095ddb404c9d094ae6a3240596d7c395e53" # from: golang:1.24.6-bullseye +builder/golang-bullseye: "sha256:e694827623f7a8bf932c3b9462546095ddb404c9d094ae6a3240596d7c395e53" # from: golang:1.24.6-bullseye +builder/golang-gost-alpine-1.24: "sha256:9bd14d0fddc0f9950385228a1fc83ddfec7e0b1f8f69e84e944901573b2bbd7c" # from: golang:1.24.13-alpine3.22 +builder/golang-gost-alpine: "sha256:9bd14d0fddc0f9950385228a1fc83ddfec7e0b1f8f69e84e944901573b2bbd7c" # from: golang:1.24.13-alpine3.22 +builder/golang-gost-bookworm-1.24: "sha256:9a1a5a2c0e1bb6d24cca379b9a3014d27750870c8995864345f20b112e7ee384" # from: golang:1.24.13-bookworm +builder/golang-gost-bookworm: "sha256:9a1a5a2c0e1bb6d24cca379b9a3014d27750870c8995864345f20b112e7ee384" # from: golang:1.24.13-bookworm +builder/golang-gost-bullseye-1.24: "sha256:8bc21d5ce16de07e2655368b322e447936513257acce2b1b133a92c7813c4717" # from: golang:1.24.6-bullseye +builder/golang-gost-bullseye: "sha256:8bc21d5ce16de07e2655368b322e447936513257acce2b1b133a92c7813c4717" # from: golang:1.24.6-bullseye +builder/node-alpine-22.16: "sha256:6c2e7b770880f1731713010d101b238c988437c1c5e6e3f23ed27d27f39ea074" # from: node:22.16.0-alpine3.20 +builder/node-alpine-23.10: "sha256:640b4b61a9f491189d7a7fa99c59bfea81f5c529a3dedb2134860a3d61fbd6cb" # from: node:23.10.0-alpine3.20 +builder/node-alpine: "sha256:6c2e7b770880f1731713010d101b238c988437c1c5e6e3f23ed27d27f39ea074" # from: node:22.16.0-alpine3.20 +builder/scratch: "sha256:1b7d59d0fd717710beaebed73c82caac21babcd7c6d22899a92a1e4fd5eafdfd" # from: registry.werf.io/werf/scratch +builder/src: "sha256:d088e5cb2f9396d9329c2513627295efeb14c58b4e053fba8c8325c66dbd165e" # from: builder/alt +libs/abseil-cpp-20240722.1: "sha256:9432021ffd8e632b0b3e481004b98d21272c8873e5e3dec2773fbdd9a8367070" # from: builder/scratch +libs/abseil-cpp: "sha256:9432021ffd8e632b0b3e481004b98d21272c8873e5e3dec2773fbdd9a8367070" # from: builder/scratch +libs/argp-standalone-1.5.0: "sha256:97bdd49fd2f5186cf5c35e36bf23701926ed82f75d50f620bbe898220699b777" # from: builder/scratch +libs/argp-standalone: "sha256:97bdd49fd2f5186cf5c35e36bf23701926ed82f75d50f620bbe898220699b777" # from: builder/scratch +libs/brotli: "sha256:95150d3adda491d029407024b7b43df51d0d7195c4a2fc8b7b43b82df68a3d77" # from: builder/scratch +libs/brotli-v1.1.0: "sha256:95150d3adda491d029407024b7b43df51d0d7195c4a2fc8b7b43b82df68a3d77" # from: builder/scratch +libs/bzip2-bzip2-1.0.8: "sha256:f5a8978bdfba46dd862b52ae41e3eac693adac98ee215602eef8b71f092d8ab5" # from: builder/scratch +libs/bzip2: "sha256:f5a8978bdfba46dd862b52ae41e3eac693adac98ee215602eef8b71f092d8ab5" # from: builder/scratch +libs/c-ares: "sha256:09e5170580376687072ff34c714c082b1327be4443f33fecd8501266e8cec61f" # from: builder/scratch +libs/c-ares-v1.34.5: "sha256:09e5170580376687072ff34c714c082b1327be4443f33fecd8501266e8cec61f" # from: builder/scratch +libs/gdbm: "sha256:a3518b62d7c18ce6a48df372197417331406fde63e81f075b165eb46b4393e9a" # from: builder/scratch +libs/gdbm-v1.24: "sha256:a3518b62d7c18ce6a48df372197417331406fde63e81f075b165eb46b4393e9a" # from: builder/scratch +libs/glibc: "sha256:64ead0b0350b34d6101684e6ff8f307afa68c923bb659c08ae14834f7d80e933" # from: builder/scratch +libs/glibc-v2.41: "sha256:64ead0b0350b34d6101684e6ff8f307afa68c923bb659c08ae14834f7d80e933" # from: builder/scratch +libs/gmp-6.3.0: "sha256:8a12d78629edce43f316c05478195466cb91350ec0647972e69b136ae85972d7" # from: builder/scratch +libs/gmp: "sha256:8a12d78629edce43f316c05478195466cb91350ec0647972e69b136ae85972d7" # from: builder/scratch +libs/grpc: "sha256:458bf5ad35b0cc57e76303e4cb037e89adac9880b15cf92072079f2333e2176a" # from: builder/scratch +libs/grpc-v1.62.1: "sha256:458bf5ad35b0cc57e76303e4cb037e89adac9880b15cf92072079f2333e2176a" # from: builder/scratch +libs/icu-release-77-1: "sha256:b6c93deb9356206d560bf9da85d11f8796c4ec90ba4beeb991b1735ea0bce1e8" # from: builder/scratch +libs/icu: "sha256:b6c93deb9356206d560bf9da85d11f8796c4ec90ba4beeb991b1735ea0bce1e8" # from: builder/scratch +libs/json-c-json-c-0.18-20240915: "sha256:2870f8a8339d28de46bc3dabfc882921f186111ff1e5d55578a7d3117bae3a3f" # from: builder/scratch +libs/json-c: "sha256:2870f8a8339d28de46bc3dabfc882921f186111ff1e5d55578a7d3117bae3a3f" # from: builder/scratch +libs/keyutils: "sha256:105a1c8e01ce6b32dbde55c396542f345022c63facbdb3b8f9e619526b20035f" # from: builder/scratch +libs/keyutils-v1.6.1: "sha256:105a1c8e01ce6b32dbde55c396542f345022c63facbdb3b8f9e619526b20035f" # from: builder/scratch +libs/krb5-krb5-1.21.3-final: "sha256:49e2b4d0ebd67199c11c8a582e44189f4c72b938407fb44ef718be628bebeb30" # from: builder/scratch +libs/krb5: "sha256:49e2b4d0ebd67199c11c8a582e44189f4c72b938407fb44ef718be628bebeb30" # from: builder/scratch +libs/libaio-libaio-0.3.113: "sha256:ad48bb81940e6cb93cd93efe1516597a8dea109186e061d1a439bab17ce56507" # from: builder/scratch +libs/libaio: "sha256:ad48bb81940e6cb93cd93efe1516597a8dea109186e061d1a439bab17ce56507" # from: builder/scratch +libs/libcap: "sha256:4f7abb1bbbe5b4c472041b9f3b30c627e9f8d96263cbdbba34872faf1364b199" # from: builder/scratch +libs/libcap-v1.2.69: "sha256:fbd54053086db03e1c7d9330c25416f58d2f7af06d3285e653c65628bc22692f" # from: builder/scratch +libs/libcap-v1.2.71: "sha256:4f7abb1bbbe5b4c472041b9f3b30c627e9f8d96263cbdbba34872faf1364b199" # from: builder/scratch +libs/libedit: "sha256:5744e228efb525b5e706777721893da6c30aa3ecee7e28513f72355cd6d13ecc" # from: builder/scratch +libs/libedit-v20250104.3.1: "sha256:5744e228efb525b5e706777721893da6c30aa3ecee7e28513f72355cd6d13ecc" # from: builder/scratch +libs/libevent-release-2.2.1-alpha: "sha256:c145a67da3187809b468daf627ace7c58d16834836c87272a14a9dbf81b148a0" # from: builder/scratch +libs/libevent: "sha256:c145a67da3187809b468daf627ace7c58d16834836c87272a14a9dbf81b148a0" # from: builder/scratch +libs/libev: "sha256:dc6390d5ba3d81d0247806ffbde76d19c46b4e71650da7f01a005b866422b50d" # from: builder/scratch +libs/libev-v4.33: "sha256:dc6390d5ba3d81d0247806ffbde76d19c46b4e71650da7f01a005b866422b50d" # from: builder/scratch +libs/libffi: "sha256:f49f4f57537001db206cac41f5f366b2a4836b99628f81ef549470506e7f2bf7" # from: builder/scratch +libs/libffi-v3.4.8: "sha256:f49f4f57537001db206cac41f5f366b2a4836b99628f81ef549470506e7f2bf7" # from: builder/scratch +libs/libgcrypt-libgcrypt-1.11.1: "sha256:4c5b46c2918046a41081d34b832ed7e6409cbdc6f180e47d90f228d451f2fbec" # from: builder/scratch +libs/libgcrypt: "sha256:4c5b46c2918046a41081d34b832ed7e6409cbdc6f180e47d90f228d451f2fbec" # from: builder/scratch +libs/libgpg-error-libgpg-error-1.55: "sha256:74bb11cb78feec083af8b897fc515f1ece0211579e7aa6095824505f3399d0a4" # from: builder/scratch +libs/libgpg-error: "sha256:74bb11cb78feec083af8b897fc515f1ece0211579e7aa6095824505f3399d0a4" # from: builder/scratch +libs/libidn2: "sha256:0eed614805fbe2da25b2e9195d549f15ec1ff373ad01cb34b039c1bd7aa63ffc" # from: builder/scratch +libs/libidn2-v2.3.8: "sha256:0eed614805fbe2da25b2e9195d549f15ec1ff373ad01cb34b039c1bd7aa63ffc" # from: builder/scratch +libs/libidn: "sha256:b597f320d1b2b8e17f19706a82e75370b462fd5d143e95faf83b1312aa4d700d" # from: builder/scratch +libs/libidn-v1.43: "sha256:b597f320d1b2b8e17f19706a82e75370b462fd5d143e95faf83b1312aa4d700d" # from: builder/scratch +libs/libinih-r60: "sha256:35a89e45565fe3ad5a513efec079edf502e246efdf3916a43bb3a38af1731776" # from: builder/scratch +libs/libinih: "sha256:35a89e45565fe3ad5a513efec079edf502e246efdf3916a43bb3a38af1731776" # from: builder/scratch +libs/libmaxminddb-1.12.2: "sha256:fddb7f60ffc929ecf1b1a85806568ba27f90692b0499ba492452ec4c0c3c4e61" # from: builder/scratch +libs/libmaxminddb: "sha256:fddb7f60ffc929ecf1b1a85806568ba27f90692b0499ba492452ec4c0c3c4e61" # from: builder/scratch +libs/libmnl-libmnl-1.0.5: "sha256:389a2e55a67ad26f14d8982e46df187771a8f96b4d4482b63495eb94a524bbc0" # from: builder/scratch +libs/libmnl: "sha256:389a2e55a67ad26f14d8982e46df187771a8f96b4d4482b63495eb94a524bbc0" # from: builder/scratch +libs/libnetfilter_conntrack-libnetfilter_conntrack-1.1.0: "sha256:6bd6da6e28604c6b1d7951273803f163ca7bdbd1ad7663062cb9415a00968330" # from: builder/scratch +libs/libnetfilter_conntrack: "sha256:6bd6da6e28604c6b1d7951273803f163ca7bdbd1ad7663062cb9415a00968330" # from: builder/scratch +libs/libnetfilter_cthelper-libnetfilter_cthelper-1.0.1: "sha256:744a1fb7b4cb3d47f6fae0ea2ba7b77b1e380df585ffaa26ee3127714e739b00" # from: builder/scratch +libs/libnetfilter_cthelper: "sha256:744a1fb7b4cb3d47f6fae0ea2ba7b77b1e380df585ffaa26ee3127714e739b00" # from: builder/scratch +libs/libnetfilter_cttimeout-libnetfilter_cttimeout-1.0.1: "sha256:ba9c2d0102ca6254f5eb1ec3160abcf96594dcbb7f33cc2878a787dfc88e97d0" # from: builder/scratch +libs/libnetfilter_cttimeout: "sha256:ba9c2d0102ca6254f5eb1ec3160abcf96594dcbb7f33cc2878a787dfc88e97d0" # from: builder/scratch +libs/libnetfilter_queue-libnetfilter_queue-1.0.5: "sha256:1acc22685296d521c4c494dea0c6243867ec98c3bbb95a506349ccaa8107c2eb" # from: builder/scratch +libs/libnetfilter_queue: "sha256:1acc22685296d521c4c494dea0c6243867ec98c3bbb95a506349ccaa8107c2eb" # from: builder/scratch +libs/libnfnetlink-libnfnetlink-1.0.2: "sha256:14c51706ddfafd244e9a63b512943026f57bf41e7700127463858123889cb576" # from: builder/scratch +libs/libnfnetlink: "sha256:14c51706ddfafd244e9a63b512943026f57bf41e7700127463858123889cb576" # from: builder/scratch +libs/libnftnl-libnftnl-1.2.9: "sha256:b90addfd327b2e435f0b907e57e8e1af80794e70b014a58e575e47208b5a5de8" # from: builder/scratch +libs/libnftnl: "sha256:b90addfd327b2e435f0b907e57e8e1af80794e70b014a58e575e47208b5a5de8" # from: builder/scratch +libs/libnl-libnl3_2_25: "sha256:ec5589dc7a6c20199e6f0dc02ba5154aad9e77143c3a87a36a1afc0f02d9a0cd" # from: builder/scratch +libs/libnl: "sha256:ec5589dc7a6c20199e6f0dc02ba5154aad9e77143c3a87a36a1afc0f02d9a0cd" # from: builder/scratch +libs/libnvme: "sha256:e92c8a4b965d4fb6814931a9b353606d9156e829f467a7986b93eba4f63340bb" # from: builder/scratch +libs/libnvme-v1.16.1: "sha256:e92c8a4b965d4fb6814931a9b353606d9156e829f467a7986b93eba4f63340bb" # from: builder/scratch +libs/libpq-REL_17_5: "sha256:27e8e97b31934fc1eae6bd6698a34b7d6fd340d8a199cf9ebca2a471e771c51b" # from: builder/scratch +libs/libpq: "sha256:27e8e97b31934fc1eae6bd6698a34b7d6fd340d8a199cf9ebca2a471e771c51b" # from: builder/scratch +libs/libpsl-0.21.5: "sha256:f28d6e92f76ab55ef5db4df4c104a208b9ab5fca100cadc499107dd17c0458c7" # from: builder/scratch +libs/libpsl: "sha256:f28d6e92f76ab55ef5db4df4c104a208b9ab5fca100cadc499107dd17c0458c7" # from: builder/scratch +libs/libtirpc-libtirpc-1-3-6: "sha256:9514f6b2454ba55886ba95128f1b521fa357e9e966ad458badc60741ef9a63df" # from: builder/scratch +libs/libtirpc: "sha256:9514f6b2454ba55886ba95128f1b521fa357e9e966ad458badc60741ef9a63df" # from: builder/scratch +libs/libunistring: "sha256:47e2fa2e3511c4c8c64e1fc103bdd97c5cce2e005dcaa9d977aa74817e9034cc" # from: builder/scratch +libs/libunistring-v1.3: "sha256:47e2fa2e3511c4c8c64e1fc103bdd97c5cce2e005dcaa9d977aa74817e9034cc" # from: builder/scratch +libs/libuv: "sha256:1b50f0bee31afff3ebde13760b8ca0beefdff1005b48247225afd7cb45a847b3" # from: builder/scratch +libs/libuv-v1.51.0: "sha256:1b50f0bee31afff3ebde13760b8ca0beefdff1005b48247225afd7cb45a847b3" # from: builder/scratch +libs/libxml2: "sha256:bc6751a22021e112ed317163e9b8cd3f7691004905bcd851ea9f80ae5f7080e7" # from: builder/scratch +libs/libxml2-v2.13.8: "sha256:c4b7f7da35c072448e3b5e34b205905f541ae9999777d0261272d300fe7f7700" # from: builder/scratch +libs/libxml2-v2.14.3: "sha256:bc6751a22021e112ed317163e9b8cd3f7691004905bcd851ea9f80ae5f7080e7" # from: builder/scratch +libs/libxslt: "sha256:5d29726517726bd3ca94905a75855344ffb74988cc8d073c5c1dff0847c44e65" # from: builder/scratch +libs/libxslt-v1.1.43: "sha256:5d29726517726bd3ca94905a75855344ffb74988cc8d073c5c1dff0847c44e65" # from: builder/scratch +libs/libyaml-0.2.5: "sha256:61ca1fc190a93c462857d5d28863f1945ba88ec14f195f782ca566caf722b2ab" # from: builder/scratch +libs/libyaml: "sha256:61ca1fc190a93c462857d5d28863f1945ba88ec14f195f782ca566caf722b2ab" # from: builder/scratch +libs/lmdb-LMDB_0.9.31: "sha256:5769c82e1e969ee966f12ec7a5c0d2104b8d83e414452aab28005523446e4446" # from: builder/scratch +libs/lmdb: "sha256:5769c82e1e969ee966f12ec7a5c0d2104b8d83e414452aab28005523446e4446" # from: builder/scratch +libs/lua-iconv-7-3: "sha256:eb11950f76000b4ef2bf392358408d4939156908c4f45d6f9a04b73570787035" # from: builder/scratch +libs/lua-iconv: "sha256:eb11950f76000b4ef2bf392358408d4939156908c4f45d6f9a04b73570787035" # from: builder/scratch +libs/lua-protobuf-0.5.1: "sha256:804ac09be8cd41f3f366f12517244f531ef4c3023ec5cbfa45cb2a41778c2cca" # from: builder/scratch +libs/lua-protobuf: "sha256:804ac09be8cd41f3f366f12517244f531ef4c3023ec5cbfa45cb2a41778c2cca" # from: builder/scratch +libs/mpc1-1.3.1: "sha256:e745110ea48a0c04afd848388726034fe64e82ae5cc90877ad218ffe02310d6c" # from: builder/scratch +libs/mpc1: "sha256:e745110ea48a0c04afd848388726034fe64e82ae5cc90877ad218ffe02310d6c" # from: builder/scratch +libs/mpfr4-4.2.1: "sha256:cf5cb11810027600974085a4370389dd90f9d1877b2de0adeb3f6e9a4f7c359b" # from: builder/scratch +libs/mpfr4: "sha256:cf5cb11810027600974085a4370389dd90f9d1877b2de0adeb3f6e9a4f7c359b" # from: builder/scratch +libs/musl-fts: "sha256:ee574d6ec7a225edfc1605a55dee0b4c0741af488a759daf1e56fafade6a0780" # from: builder/scratch +libs/musl-fts-v1.2.7: "sha256:ee574d6ec7a225edfc1605a55dee0b4c0741af488a759daf1e56fafade6a0780" # from: builder/scratch +libs/musl-obstack: "sha256:cd9a6fc44b17f1b16e9b7b7011bc59b4b997bb333d79d54be63ba78634dae4c8" # from: builder/scratch +libs/musl-obstack-v1.2.3: "sha256:cd9a6fc44b17f1b16e9b7b7011bc59b4b997bb333d79d54be63ba78634dae4c8" # from: builder/scratch +libs/musl: "sha256:8d8b96575db08844aad856592c09fb4c95e165ebfdba95d9985e06d8413b64da" # from: builder/scratch +libs/musl-v1.2.5: "sha256:8d8b96575db08844aad856592c09fb4c95e165ebfdba95d9985e06d8413b64da" # from: builder/scratch +libs/ncurses: "sha256:808cbd4350c3d2abe85979b68043e0e3fdb914bcd6c110168c730ca623cfe237" # from: builder/scratch +libs/ncurses-v6_5_20250920: "sha256:808cbd4350c3d2abe85979b68043e0e3fdb914bcd6c110168c730ca623cfe237" # from: builder/scratch +libs/nghttp2: "sha256:5c62f3eb1c35321dfb0981d419a37faa3e1c6916b4e5975fe36ed7e722ed8fbd" # from: builder/scratch +libs/nghttp2-v1.66.0: "sha256:5c62f3eb1c35321dfb0981d419a37faa3e1c6916b4e5975fe36ed7e722ed8fbd" # from: builder/scratch +libs/oniguruma: "sha256:e1f9a578dca9574145ea06b7c2cc8f308f8d566eb77296b51dfd2291ee060f26" # from: builder/scratch +libs/oniguruma-v6.9.10: "sha256:e1f9a578dca9574145ea06b7c2cc8f308f8d566eb77296b51dfd2291ee060f26" # from: builder/scratch +libs/pcre2-pcre2-10.45: "sha256:bcf54aac458981b6e247f1f8ffa19cce3df5c314cffce7231365a775e30d5af9" # from: builder/scratch +libs/pcre2: "sha256:bcf54aac458981b6e247f1f8ffa19cce3df5c314cffce7231365a775e30d5af9" # from: builder/scratch +libs/pcre-8.45: "sha256:667b91f93f410bc10c5f0873d3843aa2145f0482d0fb81f5af203cc113da4667" # from: builder/scratch +libs/pcre: "sha256:667b91f93f410bc10c5f0873d3843aa2145f0482d0fb81f5af203cc113da4667" # from: builder/scratch +libs/popt-popt-1.19-release: "sha256:d1abf88cedb83c8c5e03f36210074e5042357033ecaee74bff41b20e1ccacac2" # from: builder/scratch +libs/popt: "sha256:d1abf88cedb83c8c5e03f36210074e5042357033ecaee74bff41b20e1ccacac2" # from: builder/scratch +libs/protobuf: "sha256:6b47c6c94c8ea0d4397d273e33bd3b550af2e4884c1914e48cd55c0856a27c26" # from: builder/scratch +libs/protobuf-v29.4: "sha256:6b47c6c94c8ea0d4397d273e33bd3b550af2e4884c1914e48cd55c0856a27c26" # from: builder/scratch +libs/python-wheel: "sha256:4a0fff73311c85685df75de138dbca48cac9d72ceb44d72feb8a91d418134ec1" # from: builder/scratch +libs/python-wheel-v0.1: "sha256:4a0fff73311c85685df75de138dbca48cac9d72ceb44d72feb8a91d418134ec1" # from: builder/scratch +libs/re2-2024-07-02: "sha256:bbc7f4c9abb0c569df2ba01fad9734e474e4310e6ac677df617d84bee2e18f39" # from: builder/scratch +libs/re2: "sha256:bbc7f4c9abb0c569df2ba01fad9734e474e4310e6ac677df617d84bee2e18f39" # from: builder/scratch +libs/readline-readline-8.2: "sha256:7c69b2465cdeed0dd9fd551900cd467d8ffa9fbea21e902ce61cf1cdafd3b4b9" # from: builder/scratch +libs/readline: "sha256:7c69b2465cdeed0dd9fd551900cd467d8ffa9fbea21e902ce61cf1cdafd3b4b9" # from: builder/scratch +libs/skalibs: "sha256:ea218919c6a716457783b5537e419b707b765d5526f70d16641bd0646a07b482" # from: builder/scratch +libs/skalibs-v2.14.3.0: "sha256:ea218919c6a716457783b5537e419b707b765d5526f70d16641bd0646a07b482" # from: builder/scratch +libs/sqlite: "sha256:cf420eca89a0d495ca28b54cb5a9bbece5720e6597b5573bcad18d681029338c" # from: builder/scratch +libs/sqlite-version-3.49.1: "sha256:cf420eca89a0d495ca28b54cb5a9bbece5720e6597b5573bcad18d681029338c" # from: builder/scratch +libs/userspace-rcu: "sha256:b6e384c9bf9f31ec953d6bf2cb28e656436f95315a5d921de3680197235b48cf" # from: builder/scratch +libs/userspace-rcu-v0.15.2: "sha256:b6e384c9bf9f31ec953d6bf2cb28e656436f95315a5d921de3680197235b48cf" # from: builder/scratch +libs/utmps: "sha256:2bc67c4cec15e7c1d5d9d4b434146c164d9004da93793ed3edb68a82006257b9" # from: builder/scratch +libs/utmps-v0.1.2.3: "sha256:2bc67c4cec15e7c1d5d9d4b434146c164d9004da93793ed3edb68a82006257b9" # from: builder/scratch +libs/xz: "sha256:bb3598b38e64e6b10035c3724ec1e24fffeb59081e1f5a9c2e45cfd366c01c12" # from: builder/scratch +libs/xz-v5.8.1: "sha256:bb3598b38e64e6b10035c3724ec1e24fffeb59081e1f5a9c2e45cfd366c01c12" # from: builder/scratch +libs/yajl-2.1.0: "sha256:dabb5b7af863d4cbbf70ed3c4b4b0a551b8ff05e9b374b10b1ecb3eee6c0691a" # from: builder/scratch +libs/yajl: "sha256:dabb5b7af863d4cbbf70ed3c4b4b0a551b8ff05e9b374b10b1ecb3eee6c0691a" # from: builder/scratch +libs/zlib: "sha256:2afd8f86a0f285371e7a6502f8c0335c476a8d36b0d75a647201320c0164708a" # from: builder/scratch +libs/zlib-v1.3.1: "sha256:2afd8f86a0f285371e7a6502f8c0335c476a8d36b0d75a647201320c0164708a" # from: builder/scratch +libs/zstd: "sha256:c1589bcf4f8f1c01c0137ade5a639e02e48ef78d8fed33cf7a2632c860cce3c4" # from: builder/scratch +libs/zstd-v1.5.7: "sha256:c1589bcf4f8f1c01c0137ade5a639e02e48ef78d8fed33cf7a2632c860cce3c4" # from: builder/scratch +tools/bash-completion-2.16.0: "sha256:f76e83e0c969212ca612dc78f1301b4eee9d02d1488a3c86131bf17936f54385" # from: builder/scratch +tools/bash-completion: "sha256:f76e83e0c969212ca612dc78f1301b4eee9d02d1488a3c86131bf17936f54385" # from: builder/scratch +tools/bash: "sha256:5c1f50c38ed22115094c11fd6ead78ff227a2bacda48a99010e0dee6489f2c71" # from: builder/scratch +tools/bash-v5.2.37: "sha256:5c1f50c38ed22115094c11fd6ead78ff227a2bacda48a99010e0dee6489f2c71" # from: builder/scratch +tools/conntrack-tools-conntrack-tools-1.4.8: "sha256:0971978df9c5a73edec4cc2b53d31a4a51494a31fb10db87d15f47b4f31f22ce" # from: builder/scratch +tools/conntrack-tools: "sha256:0971978df9c5a73edec4cc2b53d31a4a51494a31fb10db87d15f47b4f31f22ce" # from: builder/scratch +tools/coreutils: "sha256:543a3a48b303cf6d2abfea7343a3ffd28744fbf593dd9686415d22b8049236ac" # from: builder/scratch +tools/coreutils-v9.7: "sha256:543a3a48b303cf6d2abfea7343a3ffd28744fbf593dd9686415d22b8049236ac" # from: builder/scratch +tools/cosign: "sha256:120904c0e4ca7f6c57bd1c6f51493245d6167defdeaf9a34d93e28ebb42ba8b6" # from: builder/scratch +tools/cosign-v2.4.3: "sha256:120904c0e4ca7f6c57bd1c6f51493245d6167defdeaf9a34d93e28ebb42ba8b6" # from: builder/scratch +tools/cryptsetup: "sha256:c6148af06a6d3cce89316d6ca61e9455bdbe30abb95e99804e02fd504297931c" # from: builder/scratch +tools/cryptsetup-v2.7.5: "sha256:c6148af06a6d3cce89316d6ca61e9455bdbe30abb95e99804e02fd504297931c" # from: builder/scratch +tools/curl-curl-8_17_0: "sha256:a26c005481d82dfdcdf0fe782edba0fbaa064085b7b4a08808e1c4767c7d3acd" # from: builder/scratch +tools/curl: "sha256:a26c005481d82dfdcdf0fe782edba0fbaa064085b7b4a08808e1c4767c7d3acd" # from: builder/scratch +tools/diffutils: "sha256:cb6227f8c67595732fe50835ae47938d9e1f8556e7772e6090866e2bce0be0fc" # from: builder/scratch +tools/diffutils-v3.12: "sha256:cb6227f8c67595732fe50835ae47938d9e1f8556e7772e6090866e2bce0be0fc" # from: builder/scratch +tools/dumb-init: "sha256:8023a218203526e7b01cf539e1889bdac6a51406cba8320c6157e13608c94ee3" # from: builder/scratch +tools/dumb-init-v1.2.5: "sha256:8023a218203526e7b01cf539e1889bdac6a51406cba8320c6157e13608c94ee3" # from: builder/scratch +tools/e2fsprogs: "sha256:65e7bb836057d0decdc5b1386455840616cce1f6bcff91e1f6427671aca6d238" # from: builder/scratch +tools/e2fsprogs-v1.47.2: "sha256:65e7bb836057d0decdc5b1386455840616cce1f6bcff91e1f6427671aca6d238" # from: builder/scratch +tools/elfutils-elfutils-0.193: "sha256:b2565b5fc0231190e3b35f52451192792c12e9111cbbad2db8dd6a821e96f49f" # from: builder/scratch +tools/elfutils: "sha256:b2565b5fc0231190e3b35f52451192792c12e9111cbbad2db8dd6a821e96f49f" # from: builder/scratch +tools/erofs-utils: "sha256:215045baa67408ff709bcde88ae39e747e25424db5898fa7cb0d5fb9a5e98d7c" # from: builder/scratch +tools/erofs-utils-v1.8.10: "sha256:215045baa67408ff709bcde88ae39e747e25424db5898fa7cb0d5fb9a5e98d7c" # from: builder/scratch +tools/ethtool: "sha256:62e8c609e4066e00fd2f6a7e5dcf654343ef975eecd194995cd55ceee0bc3d06" # from: builder/scratch +tools/ethtool-v6.15: "sha256:62e8c609e4066e00fd2f6a7e5dcf654343ef975eecd194995cd55ceee0bc3d06" # from: builder/scratch +tools/findutils: "sha256:ab871f54bbf1d95ec4f95bc70e7f8d51edc5b82c0f586b6770729353bd732b2b" # from: builder/scratch +tools/findutils-v4.10.0: "sha256:ab871f54bbf1d95ec4f95bc70e7f8d51edc5b82c0f586b6770729353bd732b2b" # from: builder/scratch +tools/gawk: "sha256:7ca875b23d356ed7ca35372160e5a4b4b8c3552532b5c8fce366d0db0525710a" # from: builder/scratch +tools/gawk-v5.3.2: "sha256:7ca875b23d356ed7ca35372160e5a4b4b8c3552532b5c8fce366d0db0525710a" # from: builder/scratch +tools/gcc-12.1.0: "sha256:43924e061e9895c005622d213690ba509372ac2aedac033081deef21c55b974f" # from: builder/scratch +tools/gcc-gnu-releases/gcc-14.2.0: "sha256:94dc9c9be75acf0074cadeefe0f5e34ba154150b018fa0f2015f4eddf5653f64" # from: builder/scratch +tools/gcc-gnu: "sha256:94dc9c9be75acf0074cadeefe0f5e34ba154150b018fa0f2015f4eddf5653f64" # from: builder/scratch +tools/gcc: "sha256:43924e061e9895c005622d213690ba509372ac2aedac033081deef21c55b974f" # from: builder/scratch +tools/git: "sha256:5f8e9cbb04c26deb0fdbac1197ec554061866435d526131db5de907cfb3a0105" # from: builder/scratch +tools/git-v2.50.1: "sha256:5f8e9cbb04c26deb0fdbac1197ec554061866435d526131db5de907cfb3a0105" # from: builder/scratch +tools/golang-1.24.13: "sha256:4dc370fd0a45492aab8435d1628a2082c328ba63926e88e879895cad01306118" # from: builder/scratch +tools/golang-1.25.7: "sha256:227f742764bde45608d1209ed1758c35b1b5313a2f8d99ee0cc48aecfda80dc7" # from: builder/scratch +tools/golang: "sha256:227f742764bde45608d1209ed1758c35b1b5313a2f8d99ee0cc48aecfda80dc7" # from: builder/scratch +tools/grep-grep-3.11: "sha256:2dbeb25b1e742a98863f70eb3b3cb3aaf9435f43f255b91093bc87a360a52a38" # from: builder/scratch +tools/grep: "sha256:2dbeb25b1e742a98863f70eb3b3cb3aaf9435f43f255b91093bc87a360a52a38" # from: builder/scratch +tools/iproute2: "sha256:46150bdc7959c3ff8395a427bff4d75da028f604f87139069fac951637310708" # from: builder/scratch +tools/iproute2-v6.12.0: "sha256:46150bdc7959c3ff8395a427bff4d75da028f604f87139069fac951637310708" # from: builder/scratch +tools/ipset: "sha256:b53afdf8b69d7a3161335780feed92373852f485bb0ac585e113392037682a94" # from: builder/scratch +tools/ipset-v7.22: "sha256:b53afdf8b69d7a3161335780feed92373852f485bb0ac585e113392037682a94" # from: builder/scratch +tools/iptables: "sha256:a63759d838499b37317be04c5f687b2ecb3a3cbdb50f1a3010998e671b30da34" # from: builder/scratch +tools/iptables-v1.8.9: "sha256:a63759d838499b37317be04c5f687b2ecb3a3cbdb50f1a3010998e671b30da34" # from: builder/scratch +tools/jq-1.7.1: "sha256:8e801f72604880a20af7ebfb3eb7238e17dcf61821f59c2af4d38d7e6caf25f6" # from: builder/scratch +tools/jq: "sha256:8e801f72604880a20af7ebfb3eb7238e17dcf61821f59c2af4d38d7e6caf25f6" # from: builder/scratch +tools/kmod: "sha256:91cf59c465a9e4a250ed91238fde9d954317c9335d3f68831a4625dec3ae5057" # from: builder/scratch +tools/kmod-v33: "sha256:91cf59c465a9e4a250ed91238fde9d954317c9335d3f68831a4625dec3ae5057" # from: builder/scratch +tools/less-less-668: "sha256:a424329717329eed9b5a9b57b051d45f3eddcc437f12e8f8ba177cc372ade3c0" # from: builder/scratch +tools/less: "sha256:a424329717329eed9b5a9b57b051d45f3eddcc437f12e8f8ba177cc372ade3c0" # from: builder/scratch +tools/libcap: "sha256:4e395c7a954005432d73cee46398fd86140cf0e5fcf69257a80c22b8e527ee5f" # from: builder/scratch +tools/libcap-v1.2.76: "sha256:4e395c7a954005432d73cee46398fd86140cf0e5fcf69257a80c22b8e527ee5f" # from: builder/scratch +tools/lsscsi: "sha256:a43ed25918ccc1d0abdf093ce5065012895ff8d46e360f81444fb382ed8dbb55" # from: builder/scratch +tools/lsscsi-v0.28: "sha256:a43ed25918ccc1d0abdf093ce5065012895ff8d46e360f81444fb382ed8dbb55" # from: builder/scratch +tools/lua5-1: "sha256:ec3884cd3837c4c18cae0edda08350a36dd0ca35b2b04ff790c8e7734140e63d" # from: builder/scratch +tools/lua5-1-v5.1.5: "sha256:ec3884cd3837c4c18cae0edda08350a36dd0ca35b2b04ff790c8e7734140e63d" # from: builder/scratch +tools/luarocks5-1: "sha256:575cdcfa01417222ec95792f0bf337aef03632e9e811da5639cef70d36257b67" # from: builder/scratch +tools/luarocks5-1-v3.12.2: "sha256:575cdcfa01417222ec95792f0bf337aef03632e9e811da5639cef70d36257b67" # from: builder/scratch +tools/lvm2: "sha256:6e001216f07a8603ee1d701b3754d62e623f14ef31a7c7752c72b98801f72b3a" # from: builder/scratch +tools/lvm2-v2_03_31: "sha256:6e001216f07a8603ee1d701b3754d62e623f14ef31a7c7752c72b98801f72b3a" # from: builder/scratch +tools/memcached-1.6.39: "sha256:fdf34bfa45490d998891558a9685064d2ad7c9c1e1e72e4f99bb6a4c11f54012" # from: builder/scratch +tools/memcached: "sha256:fdf34bfa45490d998891558a9685064d2ad7c9c1e1e72e4f99bb6a4c11f54012" # from: builder/scratch +tools/multipath-tools-0.13.0: "sha256:4e3526182d3956425f49a3913a4ab0289e26b583534db7c1cd418aa92a180aa4" # from: builder/scratch +tools/multipath-tools: "sha256:4e3526182d3956425f49a3913a4ab0289e26b583534db7c1cd418aa92a180aa4" # from: builder/scratch +tools/nfs-utils-nfs-utils-2-8-2: "sha256:f3a7eec206f18e5b45cb9fc6ca5b969dd3b9bf63b87a74f52b4a67b35017c165" # from: builder/scratch +tools/nfs-utils: "sha256:f3a7eec206f18e5b45cb9fc6ca5b969dd3b9bf63b87a74f52b4a67b35017c165" # from: builder/scratch +tools/nginx-njs-release-1.28.0: "sha256:1de97adbc36db2488445e5a02e1c5e7355aef8a24ba2e647329a1363d2f9a7f8" # from: builder/scratch +tools/nginx-njs: "sha256:1de97adbc36db2488445e5a02e1c5e7355aef8a24ba2e647329a1363d2f9a7f8" # from: builder/scratch +tools/nginx-release-1.28.0: "sha256:95a6825be72e3aedc90fa921d4ddb8b301678b5c1ab1d318f122f8aca56571c3" # from: builder/scratch +tools/nginx: "sha256:95a6825be72e3aedc90fa921d4ddb8b301678b5c1ab1d318f122f8aca56571c3" # from: builder/scratch +tools/nvme-cli: "sha256:39cf96527b63ee0c9fe42fe533c5249ca6b398a7f0083ab42e4e6adc8681fd64" # from: builder/scratch +tools/nvme-cli-v2.16: "sha256:39cf96527b63ee0c9fe42fe533c5249ca6b398a7f0083ab42e4e6adc8681fd64" # from: builder/scratch +tools/open-iscsi-2.1.11: "sha256:69332f7c54a1dd5ecc095cb221a3a94be311a197af19fbf54a7769292d38cb48" # from: builder/scratch +tools/open-iscsi: "sha256:69332f7c54a1dd5ecc095cb221a3a94be311a197af19fbf54a7769292d38cb48" # from: builder/scratch +tools/openssl-3.6.0: "sha256:293bc2d64d27266bbbcfa3a0c314fe997543eca9e6028516a83ec661b445fff9" # from: builder/scratch +tools/openssl: "sha256:293bc2d64d27266bbbcfa3a0c314fe997543eca9e6028516a83ec661b445fff9" # from: builder/scratch +tools/procps: "sha256:c3d43ed90cf407fef9f9db00973a1df6483de9e4da162b56064ff88f31517522" # from: builder/scratch +tools/procps-v4.0.5: "sha256:c3d43ed90cf407fef9f9db00973a1df6483de9e4da162b56064ff88f31517522" # from: builder/scratch +tools/pwru: "sha256:e2685495eded8e7a5de01b4a1b04963faa5e780b3e055ec50f48abcffef84f51" # from: builder/scratch +tools/pwru-v1.0.10: "sha256:e2685495eded8e7a5de01b4a1b04963faa5e780b3e055ec50f48abcffef84f51" # from: builder/scratch +tools/rpcbind-rpcbind-1_2_8: "sha256:6b874a1bd3bb7ae7035a24950958502902b0be9072dd308ea7d9c2dcfcf523f7" # from: builder/scratch +tools/rpcbind: "sha256:6b874a1bd3bb7ae7035a24950958502902b0be9072dd308ea7d9c2dcfcf523f7" # from: builder/scratch +tools/sed: "sha256:c8593139a87d91776d4f74218913c434f18ba399ee697dbb39fedf611b5f8c3e" # from: builder/scratch +tools/sed-v4.9: "sha256:c8593139a87d91776d4f74218913c434f18ba399ee697dbb39fedf611b5f8c3e" # from: builder/scratch +tools/semver-3.4.0: "sha256:d10c2dcee12c42900281bad834ac0ed0a085b492dcce5d1d6addd335ac1d54fd" # from: builder/scratch +tools/semver: "sha256:d10c2dcee12c42900281bad834ac0ed0a085b492dcce5d1d6addd335ac1d54fd" # from: builder/scratch +tools/shell-operator: "sha256:6d2ec80f05a3610f74c0a08c36bac7204b8229bd4f53533962761de56c8cf32b" # from: builder/scratch +tools/shell-operator-v1.9.3: "sha256:6d2ec80f05a3610f74c0a08c36bac7204b8229bd4f53533962761de56c8cf32b" # from: builder/scratch +tools/ssh: "sha256:321d9b04ced197458a5bf1e0a3d2d5b64e4377f4ba6cafb1e1a871c2b437a96c" # from: builder/scratch +tools/ssh-V_10_0_P2: "sha256:321d9b04ced197458a5bf1e0a3d2d5b64e4377f4ba6cafb1e1a871c2b437a96c" # from: builder/scratch +tools/tar: "sha256:e23dfd80a2beedd81773f873f3e31c84c7b9173f3e2ef1de3fa0d6af0d03d6be" # from: builder/scratch +tools/tar-v1.35: "sha256:e23dfd80a2beedd81773f873f3e31c84c7b9173f3e2ef1de3fa0d6af0d03d6be" # from: builder/scratch +tools/tini: "sha256:734fb4c383204922db3c2c9bfe5562caf1c06335ee27f1345546b57aa4b9d0ea" # from: builder/scratch +tools/tini-v0.19.0: "sha256:734fb4c383204922db3c2c9bfe5562caf1c06335ee27f1345546b57aa4b9d0ea" # from: builder/scratch +tools/util-linux: "sha256:934f825b349549512c57a735d77f42af009383c2833bbd1db9cac7e3c7f0f3e3" # from: builder/scratch +tools/util-linux-v2.41: "sha256:934f825b349549512c57a735d77f42af009383c2833bbd1db9cac7e3c7f0f3e3" # from: builder/scratch +tools/vim: "sha256:12cfd8984130986c5522e45099b0cb22d81850dafcb256f5004bc536cf3740f6" # from: builder/scratch +tools/vim-v9.1.1236: "sha256:12cfd8984130986c5522e45099b0cb22d81850dafcb256f5004bc536cf3740f6" # from: builder/scratch +tools/xfsprogs: "sha256:ec14d7e45fca638728c198b7eb8d675934e777dd4cfaca6f914eb543247c9444" # from: builder/scratch +tools/xfsprogs-v6.16.0: "sha256:ec14d7e45fca638728c198b7eb8d675934e777dd4cfaca6f914eb543247c9444" # from: builder/scratch +tools/yq: "sha256:4f294d46559f45bbd7d20f2306e2eaa2b6ec1cb6e826f906377c10bb9eea04d5" # from: builder/scratch +tools/yq-v4.45.1: "sha256:893d67cc466e2be16006f9053d43701cb8bd376cd6864547ca43bafa08e01127" # from: builder/scratch +tools/yq-v4.47.1: "sha256:4f294d46559f45bbd7d20f2306e2eaa2b6ec1cb6e826f906377c10bb9eea04d5" # from: builder/scratch diff --git a/build/components/README.md b/build/components/README.md new file mode 100644 index 0000000..1fc621f --- /dev/null +++ b/build/components/README.md @@ -0,0 +1,14 @@ +# Component versions +**permalink: /componentns/** + +The `versions.yaml` file appears to be a configuration file that maps specific software components to their respective versions. This type of file is commonly used to manage and track the versions of dependencies or tools used in a project. + +## Purpose of version_map.yaml: +### Version Management: +It specifies the exact versions of software components that are required or recommended for a particular setup. This ensures consistency and reproducibility. + +### Dependency Tracking: +Keep track of the versions of critical tools or libraries that their project depends on. + +### Documentation: +It serves as a reference for anyone working on the project, making it clear which versions of the software components are being used. diff --git a/build/components/versions.yml b/build/components/versions.yml new file mode 100644 index 0000000..06ab726 --- /dev/null +++ b/build/components/versions.yml @@ -0,0 +1,4 @@ +core: + 3p-helm-controller: v0.1.3 + nelm-source-controller: v0.1.4 +package: diff --git a/charts/deckhouse_lib_helm-1.55.1.tgz b/charts/deckhouse_lib_helm-1.55.1.tgz new file mode 100644 index 0000000000000000000000000000000000000000..73159b8f03a315ecc2a8f65fa75488f44e34298c GIT binary patch literal 26935 zcmV)pK%2iGiwG0|00000|0w_~VMtOiV@ORlOnEsqVl!4SWK%V1T2nbTPgYhoO;>Dc zVQyr3R8em|NM&qo0POwycHB0yD2(sF^%NL7S)FC$_9ll9N3v zGarK{(cKuc0R{ji_t;+RJj8jz^CaKGjRZ)L&6ZlSk`ebFM@<4%g+igK02B%-O5V+A z!BL#yaXdrWeEV<)nHVkLJo~TpJUcr(J1?F+Q~%xB*{T0`XK#1!zjmMR>^^_K`}D=` z^Z(k}efIU&PyY+-tO2FSG{q+lLzMbC&W0WWFK7ZZKQ}N4Cg}q{sCok zPzaoHAkaL|pgtc1oTGiv6s!%g$k9H?@R&ii*toK2 zcSp~kk9IdcHz)oxpTPLHea*LHnB&;=Qw&KOzr!RQiF`%Huw?u{eg5L9KmPajc6XjX zj{p03K71I0?XSRF4yR}zeE0w`NwOkEAd<46ghF%J# zMFa5Z6ZmQy3_pF^FaQ}61tS~?Rv>^UK+F)}IZE&ZqZBa2X~7Z%BoMHfK#qo(a6~v3 z_zDdG=76D`A&!VZDZoSlNV0+p#9UNLAGK2Z>C*-Pve*|1B?7${Kt1~}0VXMW4@O`| zLIQ^9ORME<>1!-zVDuBr3dFa-NLj`$fJ9ea}~I9eZ7Vk$)9wF#y|?r1SD=z2~p-hGURD+z9kI4Ajxe(5Sa5B72=j~GJJ&y;yh<`jBW*~ z8js7sJl_d`S>i1DBR-x(f+xuH{H4K|;A!1YYB(qDkec3!=a@)JtM3y$^_`2V_N2%% z>wK7^qw?KHNf9hE0iuNCVMU3H-|5|BBE7a$!bv;1i(vulWR0YL|Y8`_Ay0x|z z(gu3>Z3HGzTC!$kL35hX>7rh}_P(9;f*kO3O2u1_SVg(}wjDWoFBm*z2nzHX37A3w z>vXgiX$M<%)yW)Az22>V`Un{Mt-uVWAZjb4rjojl+x3o~y2Nut3*jOPe02os#mKebYBI3EB}`_c-*;duZ`;A<-+n5Jss z6=sJVAC^*nM$-dAsDOG=)@^043Kc#*AcF|Y%E#oY4ToPQ1m?mhwKfpW_iic<*mNls zs!TVSpOgSCg=CBM0rr{}MD&Dc$}RzWcCZxM*})PZXKHr51dJ}!1_)wOA4H+=jv9Am z?gz@nDH7L|z4P>WTN!5%NXr#Vzr3JMfg0y8x|gb|zgj$pjZ#^J4|-FW>BZ zYcM9%PvsDP+J$f1sv~*vIO~xf#EBh zp(#pBVm;TYSA~E&DOZb%F=7M>#4phNCk>pV`ITB1G^-GL&1p2v=on_B6!U~$A+{J* zb?wrd1K6=e%I2RMr|@4C6A)R~Rr-O)?*(F#B~^{6N!aK${1;`R_n1&N2;0cgS!p`e zdTT6B=zLB|jAg-?(0BrIW{E3Z;b3D1ZfmXa>e07nW#khv%)f?coPP_I?8ut^=XNVzf5pUHr2i+Q4RwLeTSmn(0?7EAp#_ifz!@Q1$ z1{&kF)|=5UY40ny?5$eb#JYyIhMVHH7Ec1Y&R^c%Y|-IJ&-Y&I@Br`;%peER6=DJ> zFdODH9l}CTp1=&TVZu_i1+y_64maH31X~7^sk55>4wG~r{A7Sh&$a}$F*g&={S5$g zOu~l`&Z2>$UjD^%n4o=-elwKc%jIwt1#?=KRIO}da|XvK;|<1crZ^EGf_WY_Rv`uh z@DUJ9QbYvUlQ?*e5=pCoY>LD=n(Qm~x=fIu`1aLqq`v6JAE7*>i#Z}fy=-e~k*81~ zy&SIr>o>1yiuOSy`6i*-B_+3DVvW0ci4oVYht-Euh16a9i_EUx9Fv1Aqt__iH*adJ zY*{ZXc~~*&n`+;14&OJRs^zb}oq74fTLudgY|T(_YC}t;cR{KhbG4-<1GU)@Q?7SA z0;{Wc)r@irw`!HYZMKXJ(klGG}dERWGWLv}jdj5*G z)DNFmHCC0pVkpmW0=ecAL)uFJfVrS-@e0qe*av$z<{toRD<4gbkD+5j&rzcG3Zxfg zcA*Vehl}#bJPfTkoS{V-*qlLb7MY_YqeVJ2fjDd!g;+iSoBbADoroq_z1{Z-SP^*z z5&&IZFT)Iz8A{&qV(x9=>5zsBj*aLUeG54{gJK4v?GlPoFcZ()!LAX_>Kmy ze&aK^_xuGf<~}zaMH19p9gEFQqgGhc4rsP-mXPGUumUZgS+&Nf<)w86vV=C%ONcGS z&rXgSyg*XKl~6aR_P=*h3RJvuN$I z#tLDegWNKK4tcRmMjBcxXP7c97IBh6F4dMO@(fk-7x4ubXg)MipTmS;(3F==n7#2H&H8EEDl%I0;oss}R#&5#a_3ULyW#ZZzFf#@2G z*{~ogh#zM7-zZ(iPk)bi2_4214zhfFxOTD}l;E6glI_$9euI1xh& zFxm)>oo=XlJFHHsQ_pPY3@2RrXfM%w@vKHghM;s~l!n8Ugb1w_8I~UJ%P%%h4_+T% zoE;n<50+HQ5aei>7Fy4}e7eJzRA7jp>tTvunqh)o?)9i3*kYI?hH3ipdA+n=$s8sy zhY1#ox-gs9LwZdRV`wtW&=tyF?z!^Z5i(QfyDB=={_xQHza?+mdh1$65e&sl>I7aN zn>NP49h-<;xdU5kCo>ky_W)eMtU&uV&(u>MnwbO zuFOrfEX!Ro8Tze2TyUTCP9uWy8JZ&ovsg~!;1tCfo$^>Z1+fyi2<+}SCS!NcQ(c4# zu-Ud*d&7nPR@(co47*f%gTnIKF57-?OY3i=u5M=Q(PX!>u}-1?6b&BGj`dKmyHbsh zD(MfPl6r3cwCW=VWb5Wm)g9*h=T=%?Y{cq1s}{WasIqRNvbf92M^*Kxs+Ov%emlH9 zG*yQJ3-04OeWX5rZuRBHKlRgIwE_CD6vVDA-nC^|#XeW8Z!g%j%-OQt-5#^8A5R^% zM}gl8b5}TZmkQ|@!My@$|EVgbSHQ(BHB4(*g`;MwGM`=7)Z-9Wcb5WZbRJ!%ThHIcm4%-bo6n{q>=F$}o5N^!IX7f=*@G^1Q7 zh+)1}3;OFto71$y>NEIp#&;`?wMu~H(iQV6OlZUUV*Sp4)n*+xQDsOr848rfe8FRw z&M}Ge5>-H-g(Ubs^k_ha9hKTVtlptdAINMjbck0!T{F5`tre$Eo(guyrqtWtBjFF% zTo3jcf|;28+y6wF@U5Av)t9!|IsaP0>oafE6<2Ka8kOA#oz~+!$^aywAnyphCV(Nx zDFIg(>L3~L13$qvRSAy-WIEfIoM-&HE#~>JAjM3DDlMv1UCxj)Caf8+SdVJF(kIx0 z=1k1b z`>c*Xw*eY{c0q8M=9Ar9pncX=dZ`hv7EnlNo{Pm1X8YiS+kbbjiEgWhLRzS$F+OjW zYoJw>&kA9*;RKlS!wqseu`x`fGqYO$CS5CVV5B@L;>SV;4nAm`)i~^Mn?nwx6;C&G z2X3lZxQ&e`;AQn}Jn=#`>)$SP9$V?E@cr%WDHgM0JWA+%yE@A&=g8a09C;o((6+}J z9dGOS-|bYLBXO#2OO~sseCM=@i4i}i8S;8Tm4$_vQHK9r&-vKwiu9ZP2Iuz^j`u6> z?T0A7+umW)R_$*L0nG9Dj21cH2fswoKQ~;P3x0`m*=jBj5k_07B3Fow)#oV^(UzPK zarNJ8D3Y1{L&ws|zfzPTfuet|jw;C@p7+G357_p>qEBG-0owp(EQaar0BHSP#n|^^ zR1b2|6(V(NJGgE5umz&Lr444SZM3X0rn^n?j@GOTg|aV=wsvD$f*|3T&~#m0(I3F?bXok$1Vbi#+ zffKg>JGk$3;B8H6Kbk(P_hanq6Yxe3bxIJRIy3)-W*NQ4WC~Ofm8jeq&6PDEe*(^=X5+lPSLN{H}xYKkxWOr?}B#u?@_sdk=79vXEA?5PBR$%L}GZqE%&mUSaEIJ~;cRd)hhFYF1p zoFVZ2@g>L|HjGh5uT`r}gC$?CVfIe8xyV(s)maNZ!#PN15KCDMGjAj@EKF|w$pj}@ z4hmT|Est-5D-5MaFQ7n^BD+8WY?l01W^;-JIHTzRK$1G=zQLwjLUtxiRS_Z0)VV%K zEx`#2CRo3Bst} zo>ZqAytk{*NG+V-$#m2?%<=o3i!PrN-QhIiE_~s2w5fr2TW=cs=<|oRK6uC5 zVZ+1lx*dWZg3J9`y~}$u@DGLQ55l^yI3y|~zVW0y`QyG@-L4xbzZ0`wop$>DTJ;vY zx5j1VKCpvxg&RQ%QrT^k?|$w0A(;Jp5!MH9^(!ts45MGs^Jw$$%jQ22o<9hazuI{4 zX6^1FmRnBe9Z*Y5zvQZ0TGBeKy?h9+fq%m10k{Y2-T=M9j_}%cIB&i&yw*L{id)N< z7b?xS-VJ3-SZkku-@T{L_xAkze|NurzPIyu|L=V~Zs@N{RI7u%Vtd(k_>$SZ zo_EzMh&KVyBcfTszUBS2br_{BxW}7mbza`8uKeA&__TT%y7IL8+H%>c?$q!8pQBS3 zsPi@i)%n~4)=!e_-D~N8ahe>y(ejk0sP(2p`?0NJ`-PI_*S4zd7fF_1TgmOZJkl-D z4WR47$VRC)P_NwcZv1ebw!>={t$?n06Ji@RHh=adV)Krc`Nf=}ck@dT?GaVgc_h6M zP~b%V;72H@8C`_#Gi)#1_iEJMcVmKE+!cB23m?5)`?8%qZ;lNVZD&lP%eUY6S^wI7 z)2A)nyyMgT(sjG%+Ap2-RO-1klS?julhhvhHEwkcTnPIbE`n=o)WAD<7{=}&^pnk4 zKEO6hdgID2XS7IT%Hr8#%y1f;^#9di!@-JI%_S(G4HJ%|l`Bf?JmEL0Cz0N$BA5S6 z5kU+mHv%xr(ajJ)y&1X!UL)+D9{uomA7I~`c$DwAzMB9cnp1Lt?)xIZ=CMf6V{3Kx zR=BXLd7qbL-B2r{VL4gpeYMxpsm@Jr`Fg(6rDUrASEBijN&Mhn3|5z`-^m46ONNz} z_lwZ|Q#6wb<*h1ki@U8J;^kFMH}3J;s^v~`$8qi!&v;!|RekvE*BQadM9xMQ6=aF; zpz#)zivvdm!(@7q%urg$fpjt@w0wR19wmk3lAjrY)Te^}=b;{U$M187^udsNj;SG_ zIbu+<0GyE8M*!ZT#lAu^D%U!tvmgh2qpZqq)j%Lo97-uXws{)b3Y;M}hs0jR8z`WC z;KtY0oM!Z!qbf>;H)Lb!Dz$x;4ypEK0|nYX)Sc003FB=Bsy=P0(C`;sCUx*9kXo|nXJI&9qxnDQiZ(4_eeP$w<;7)$nwi`R7}jznS?J=x`cj5h*u65) zU8@LYF=@hD-!*~lSC9o==&(rVd54aa3*l^N5!c~bKmrf*2LIHy#6SL)$fF-PfE`-m+XHawP= zKeH1$p!8I-Gsl{)f1mE+2~ zuzi^(kcsi8i6b9VN3Jz>q#yKQZp5eug48_t&^^n2>h->Xxh0=vt8b&`Zc=x+CN`+; zwvE$kP5xrrRmEyO`N$@)tYHsn*8YflqY0?g6`I%W*S8b#hUp7#8*3d6Roy|1tGAoY z?DPQ`>?#<+1EM6&`$pNYL*1&z+qYUw`e#f9~x*fByAj z{O5f`BMa@jI+h2i$H09t|6S?8I3rgs?cK+sn{=TLDzR*8ZBDB*t<_98Im{FqBtwDyRkn4nZ<%)6* zbc%9BQVu8qzbFC#M?zpS<^SBYhhn{ry`0gsNCbD!$4YsgP&VIAX;Mh!YA9}B?N(LI zK6Sy+&`gA8VxB$8A%k-y5aUDbar2>~V$i7q>MRS!m5*e7n=;+*d;?oRdhozZ&9=-T zpe=wzQsqJ~cZh@YIHO_?O|!I0t!3lyUvs2+-&i796~ajN*_nk^UT;*&&PKV=>_&;) z0J<|{uvp|r-6MB_rQtBHfl)ljsf)F-ig)sguZ>wvJRRKnf>2$2|0U1?w7Yv>k9I~o zqumHZyD$DS+I#+NWd0kiM3H%UMCTBb)!IVSi(*WWI6OHzU#X^<$vU-fFB_Ia^(l^h z|5B=v1d8g-8gRI=VfL=WHz859H9wj_=&dLuVT6XntGn(WatiiYpKkwehNrU_UO}AM zT;4Zv0hjrI&z?W4`+v`NAJ6~Y%hSXE!#tkBSkPDv)9Rqz0&zfS{hZzQSAZc=Fv9h) z2GIiXNUAwIi^3d8xyly6NLGfnbmZCp{ontmtQ-*vz7eqbnHA&N4{Q3I9P2i32+#Zby~*^`YZbtv;jC6rdS2tA1$&MjeA&U6`cB zcMDw4a59qvTd0-a6pR;OO3N^40J%9R)-|AA%y%(i`a_K_w{gS+l0W; z{C~}%n8~@gE%djL39wTKrc41!fd>*4415r0O&8()aC@ zJDC)8<_K9R&hzDjXse4Td>97C$ zMVdC0{_~yJxVFJB>w#Z0Ivvtn@X;K;A5L(Fz*oR~$ML_>ui%;%St?yWn5NqI%ORIu zpto{bU!1bE4(&a75||=GAyAj*29YQXum_ru>%|vFh^rNUI@f3rUr$I`7lkv z2t1c;TsFS-LuGv#qZ`2V^18+5j3v`OgjPX#E^&LFD^uIa* z4n>(oO3rasFagW-|L)iI^`EEDo;~jW-OJNouY`oaodU?;L*xE*y8qGv)i6}TkTSwb zX@Y_)eW^k_opm6c&7wRtv6&zyY$A~EP^?bs@5@1#*p@oznj5B2ZO=mcwVfH>x;U`o%LH&RTY-IlZkbcWp?0Zoz+i)YD7_;_`++ngcsU;R(Lu#f}69kFLb=+LBEWY*8W2r(+yw&A{}$U^_BBp z8$N;w#95;$Rq%t?ypEQ+M1rV6WfW*?w`8>RP&Y3c?Qnz3S|MU zO40^J?eQ*-;#5&GUo@=5>bkl%f|ri2Ub4~$dq3If!wfsl4do`WcV$=nrbLHox_aow z7DR7Iwm2bg&rhPkz{9P2_$_%y=rsWq+xD%s^7<}J9we#Gwb$h90Bo+F?ZKU12g!AM zS60w#u?|`=E(CC)SjSMsU)d9^fn$VI{J-lQW!CiXEt2#GMijUIAX#rPhh$W2fX0q(m*od0V z7$0NRuf~IEow9HzFkO~c0Ug~Zd{;#Md+nO#>hsovql)6Ki61Ir!uoA#}4#h#jDZfz2yg+R}rMkZbJB!f&^pA=8AfEi5?B=%cMc$K>+ z7#Z7t0<=m)U^2H%cDZhj_4+9|pWVEpm!Y}a?!6<)ulidZrBlUpc%9A5y&iR0vZOI* zA1Zrgv0^f1i1Wh?a^8*El(ea29Yy3E@B7d^&v#?pViAgr_dC*19|01H$T;9LwZ*An zC=+c!uV5vD#N3Gl)+gt^+>;W&?^jZ$+)CfmQjiDcL(|bUpRLPSXX;wD3e6X>-WpmO zk_^n1%CX)EQ87DRamu>1X7?{<`^mxaxsnektV2zYL(ZVq3QTO6L=}&!nquCXn5F)T zwT;uuqLWEqJ5p>8v_$f_K=ZgUJk2CwxR`>As+bo&ix6EKy=9T7QUeG1y( zlz!m|>zV3a4J1=+zU!oPaWYY;!fvuZ`!EL^L&)5O57u= z)*mjP-uGH0FqwjJMw550091bA|6iR`@`$VtlMgpKUl6=PIAjm{X?;8a z6Izh8xB9P8{|?Q-jTNoC?7PfH%xeasCl1Gy+{TvNo91t7{WWXg)phDEwd%U`>RoEq z4&A!sI?!9_&4%XeR;4%4g~6?d)fA(9#ro~_;)Kz;IVQ0}M1W7j?@&SBUVB8E2hn{v zjcvGA@)+xKy1KSnk70LZXMOfPAjhki{4i0Eg5BFD-i?HQyPkFWp9 z6qEOM{MK%yOWZqRhDH?j!N-0^!>w%P$dB}Y(Gi#}$tulCb&n^QA*h0NH_>Q10zX__ zUJRsyQTkwj^H^W1Rq!M*wEmaKZokQS1L6o}T(2i@2N;tvmr(qW_;iZ{+{ld%FAR z|KH1Fbk~z7Prz$DHMa!r?v8f%MteJZJNw`q&FK{aVum@Gp)5yC>ou5V01FNZX*oE6 z6M>j6mC*Sd3&0ReW+(;cGwCSOH75N+ttWLZ zB6+u+>-_QxFb7QPIgYPTwouKRVCe@#l7iISJdF`|j#g-l{fx`3?%|zP%#}XF{+jV zwfa_Tbq+zjHcMTyBqZrR_$A`AXbVKcMEM0hE64#qrR1Dav9E8qsouZki0y-&>c#hr7P)?-5xk}a(U<)XopTQjegL2@ zx+STYcTp|vQfy(^+{>O8(EssO`vkd8H z&!0ZuSN}c^>H8hhD0vrWm=y2hcXHf&99KkYRm67tWRMp4(xBtLTFa5t_zW@5OO#kof z*6;t?*?YYI@qV7p*xTBEb(yW87Kap7*KXhm*nEdcdcr|u(nv{_W02C5|+c7+oJ zCcmYm&s3mBQgcS2o(FfEi_IsT*xPQS<)<=&w=51SpWGyHIcAKqKmF-XpwAtRD#eQ- z0{kQTlK&$DITQjhB6~x3m)9wOin=4pZ*o(l&(6n*#Qi?K^nZQDZmscOdwWlxHT3`2 zdyny7_wuZu|CcS@>O@qPW!T->*-`&=n1nN$UijNE)qP-GE$W}qR4ZnavrM8=dw5eu zi@cYW=u7x;4cEWFmE<*I9n?ON^E$Bged6IJ;GsIQ+w8}N!-tS8~-HCx%()D>(a4^RN z&taxEu(6zAm*=bm)lm@(4M@xoI4>#GSB}t$dM)Lw4D`Mbkfe~My`<*Wd|J2Uz$~NJ zXAEEA3{BB7Phh6vTB~@T1mg6^?qebtQx^r18Zm9g$ZwnDjb?lQI z)UgV(U2uWs+DG6>g`TaE>k=W52!TWhDuhZ)=}Z#3;>GbRiCdXKD6rD_i(6W>o~>QG zYC|x6L$ZZbZQqqhOB=M@^TlUBAsMuMB(H%wFOd|&+iB0e~JeR3M=wIze&4n_a2l~-HW-jzP<*Rpl1 zw$I+7?dIDR%2>_@rSr8?->&Scz%28|f|`WpDD8Ev*g6~ez%1CZ`T4=CSD#%nVo1^$ zrfCGKfM|UIM z>0w3#++^dwZH@Xrz#`w-_RGWFUb!WQeusYF&DNDaca+t`efq`!nG>#elmGMSi|38} zZ@bSP946hA&4O%R9|FVU8@SdFc~pTQI!C4L`>%HpenYBK`)#dz=7RIL3R)} z>~TmG-2SwF5^Ip{hx9dxCL^jY(|}RW{jsM(gkiKMhrZK0gB2dPd-h-I1F=qYyG>(( z-sg~TOo_@{7t_fklij!f((H~^5Y2H_=dhtHrROh6MvHW4@3tSpe-{izjn@k(TiSeo z#q${oRk%vh$iL%f2Dwi{ODwxdDfrrCN%iidS<>{E$`CF)N9$bX&<)N#B^*nRaU}fS ziOkT@dfU6G++=GhK@W9u!N>q_#`!yBg&C7#p3p1A79+Ez?2T!k+32^MK`Doc&-Om8 z_mf#-J~Z%$CU zK282rO@qpTULr3m>nev?!3AP|&C@rRMro$o`U?IU96V8}Au9#8GicmoMH$#sr&T($ z3xTMekZ3Rfo3a(R0ilU#6PnhFgXX<`>lc>p%c$z}BAYkCwNoxB1GNh9cR0@)hdJ-S z{x9?Y2o{uKGL5GUP9TAC3i)hI<>aNeBiQZ#?e2cv$p8LqXXn|Y|92lx5C5;x^RZGY zDuINYPpTwq)HxFPSGBH_V)i%iOHwd~i2C))tA4(A1$=?0rB?XqM`0 zLwE%Srs=K^ec;e<{NEgk*_N?SrJ+;HD5LK)IyN^1d7O#1zZ(6ESGl(=4cNIPMqkw! zARTf62DMx>R%Uh08WG67n9;$eTI@_Xy4XGlaHy820Z8gpW(J=+o2{#C8ADU_{#;i* z$g)>hASV5YfBZu+<%`H%QyE1afMh$p7Y;S@|AvwKX4H5St2f;Uc!m?y#M6T@`gY7^ z@T;3>hTchHCohCoXJDL>fU@V9OfyvJv1Tep54r7pM|1{lQ-|A$xyZ9Sk#dn|U6Nk5 z`@xI6$g@X@e^?U#1uw=&nDKHsqrT+fj)jI0whsoY^nD&lU>Az-tpmcZV5xfvV4h3L z%QvR~9FRN8h&4^^-k|KW?WoC!^|V%v{fFOJzqJ~?CeNb!2nvFK8|dxb+q>G1h#C*Y zU<%OY{@rJPu9K9`&9yEczM4>O8wA73!`{BRi`{-f$L$?zJOP_MR|7w)WyC0FbdJOf z6?~XTg)*@F`RnWPsFRZc;as)<+tlI+L{$gtW#UMy#0Y#*ehE(Xh`Or<0u8#7lg@5$ z($QlIRjtb0Y9dv$zTRxAF;xmJ$ya`YH`(#&`%V|~Yx+)#t^L&0{_w%W>=gcLyMmE3 zn!_m+D3vg(Y%{~^!!W8fv!(SfL95H)LzBM3r%xNh;cx@y*rWz_vSO;W=iQBWn56sQ zdo8$SLypRJY|N2>DHL#j1DIsL={G93c$nIrSxZZSF^h23K=oJ08zU-`)7yxSIbv6O zhkD`H;O}r=6(1&)m@L(@=3r1yWz2Jw$fjul(b+bQuz2YGz1^Aga|6WO9 zc?}D8U`Nxhc43y!pzT!I6i4zt0)ZHTYyl#(KKsT6byk)3sk6$&(Lf@l=Nc z*pEJ)2^45b*`l4>cPD5a?5l)^GCajnU^K`MvALmG1TwYuUjMCzO3YL;r~Y98%hrEi zH17X-`r!6fO zZ}m=2%M$8>1cm~QYhO0r^XWWSwZeW?m!hjg>8=tm&vh01|F2nI`dicMN6b8$fp*tw0pauAzJ zDN5Dtrs=t@Ey0YKAFbuK$tgc_LqeRwUbxKWRO*QKdQqTfqk)MT8TeouF)2Rzk(3-q zVmH6!0~HYT=5%uaHaF++{iejmH5$d%KfRQOg)&_RjyWQtUT6^28z2!4noV{(Cip6( z9sS?!%F^r0GUmn8v#acXY)X6fQRLa+aT}y_8>Bw9uO@k;gfc|crEt|E%~`4WWKRVa zX2O%@(l^xwDp&QkQ)zwSOr?+OKXfi=?Y}q zs#$G$R1&p(_hZ$??>Zg9iSCW(v(D1Jvs0i7bZ>z)$@=}xGgseDp}CxsQ#M>-Tjgeg z6m|5>k}Z|hgi1g*MIz?Pm5c?RBU*@cZp(%>tL>a@y9o_%8_hKh-|KIb+_}_bZ%!v94F+5r5t5iA4 z)aA&43kC(6E=HZILkTm1nYk=giHrpiTM(7rC~v=zy1h(OFkx~;k~DI4a0DWoh$fmt zE)a`=?*{3~ruBjg@{Z7JQqm3n5q){}k4W9N6b?)oc_H?W9zQqQ)n|ybZP%2&%P34a zNRjfRDCx;4k26c}FI>khvX&2?9{m_FO+EXtqRGD70+gsT&i#8Zf@Coy`n1tC&eB9} z=`A_Rrd)ql-DdtOapmXwRo8n6uJtrf18eO}eqiZYVu=&kb>)e-8_)#2lUeF4kL`4? z8)^$V9=Rjg85dT@KYiM8f9u&>M|n48g}3Hvmhyw_51jV;<|0Q4p5Ww^rs$9sM0~qA zrx^;cN=mR8s4?QxDyN+^MMFk2pf@l$kSTrCgSpVo?8L=ovhU>mNm2gCbE=Q^qd%ecr!SAWC$!$ZG{iwW_a3 z`{(>5B1W@eo#vGq!?~QH34U*SH`>yd+s)|} z>QP>Hd^BpTVfi}D=8P(JSeuI)n7Du_*d%~?nQ$lyZApwz!lwTh7x-n4;_r|5D znxR(~OkmDql-XybY zrM)czK@RPpO|4O9ZA?h54uiy%YmLqjE^B*Ul#0n2HSeGww1lbkbde9GyH=_vvB*)~ z0j`!V1<+Yv^riD!17PJ?sBB1bYCcWz(DS@>CLNRCs)0HI{4TW9);Wmw6_iK5Y!aJG9tuT}07~8_P<0>ln6h8Ik5KC~N>=BLMb6bo?H3!J|f% z&QeDxL$U#fkfb<;0==1-6GjJxpge(DMe!y57OCD#~RIGFcA1V zSLg`ZjX}DNx&;)&>pSW2%7m<~C!PI|W&Id--yd2pmJPa-9$5QGa(0=zxgVN-JT%^i zWPhV|4z!RSjJLQwtj@_Do}T=fE3Kxc(K(~<7eG+$x`BjXXBt>8`lz*d<*2}koIxBO zz6K=%?YZCe0(}o(COk!}vBLLl{7nt7Xq#n4lL<=1K8Q}~g<(%rIShgMZR?C`v}Dn% zr9p(o3CCsihMivpQi|egk$BYN9t0&!`FvGfN5Z&SC`=d@IDwffP)dx*kRg~Zj9#G3 z{811&byH_(3f7qLu0f!Dl5Z?fHLleJ%J+Z?6}qtmthL$}Y zB_*If<#L?%DOM1MU^0^v*{+5naZTB~VL{*(#F;AHSCpFD6{|>7XaI^NxS(!KK1?YW(a^bkH>lewD_8XT9rcIF^l1<5+QHIGF3w7`#qg)Js zD<~`GZAB)OC1{B05HUtsTPb}8YA7erLwbeS3JPSX)R@!P{=NdC;PB$4TD9K-*E5{V zz%}H+(bKvgjTYZhr@v?L3d!mT$BNq^W%vp)z|a&+)vN6p7I&g!RI6^GT(okpdd``u zUP|Yva}M0JX_^OOIHpuc=I8l}rmb&(*4D4uJ&>xGRd-@;R*$?MJv@AU2ffotvT}vs3;5;K|?>cIW4+l?>e^p@p<@9cl>`&(>TSP6)Iap zJTB5H5-XnrSaSc%&a<7J`u#6YcfWp&|G$r?hX&2lahyUlr-UOB%=~A!oX(EE1#|cw zIIdpzsUsh9FhFYL-lMX5VALGGe_K(r4|aFHyz7+uzpH1-_-B~v8^>;T^OJs-jsF)r z_5A<4J3D)il+Vyy&JG6oG%t~}F%oA7C9O5eenIcyJkQWSH)lfRe1Ch} zn~mp;rbQz7b~T3^%3(O6Y`&e+q>w}^NZVJt+h%`w8*`y|)163}nV4r!svAvhc$wO2 zAC@hwT0Ur8|8d;fS@kPG%5JqwOe*!)^PSf?Y6N#yVZLtEOaO$dc8A_dwu{>9ua@I; z!H(8xHepup@mfDud5{KvQ@q=-d@3T#r)c0evP$sNRzasX83Y>K4PsPwO;mEyt{g@o z?zZ!=_1>vo=?zxmb-Sjx3tf;i#1ZL^J6ehSR^?DmSpEWEwsZ>QD`VNnX5?1zzJU!wF@Yn~WADRmt)u zy%lYe7jZ$b;IW|byaZR*v~DX0%V{2f9ephA#k0O#)?IFST&4e;N1^5}V{gqEb@N+q z&h_jZUHE1J(9w{;ay9G%XQq28!A#N^48croiV+kxML{&S3a@3&u z7NS(LyOO4;+2mD3siwg?w4z@>fdWLw=jU(E_kj&hEiF#PMs*wgK4|bwY15B7%yn1` z3U2x$p)iw`or0?z#ym$!Ic?A>hXBm6Nt*w+^4&X3((0F-YFEF?oALkF;ZO9LU0mf* z9fE0dBp^wF!O74BF+`H?h0P_+c2gZGum@)+CLLe<)WRe^IygH4hD-IVNvgn)n50JD z_G^xG9h52>f}vI@8Zh=kFixA7UT7GHwsU}gfkgmD*^i+sG#ZmH2imhgkgKJ+c8(_M zMgu23qe=)>mlDOF>b?c1>(!SkF{9I=_32nZ5e$r9_36`w&Rcp$Gn_0WN&5F|r9>p) zfO^%Mqzr1a&8=XR9j#trqz)MltJkMBvMU)fifIntALxSw`$hvMdC^9Xaw*_@;KxKO zFiK6gw!>Hv_`Y6qj>(GXmBqag_GZoUm{_V8Gxx!{vHk}nRW^s$jyiVrVf1D)iEfuA zaW$v!uhNNDUjmc78Om~`6D;YgQ&jrEvIYzK zde5}bbklBmx3pWT65yj|f0*xP`su)ay%hHA4%kby`_ub%^Vz^w2;c_X4rAW?} zB01}WWGSBGrFf2e;#rF7N4yl5Q>2D#@7WTmS`Pem7x2D#zF&^#`<{6EqP$#=@^W>Q zeX*Y{$9{Gz?0pG2UQWpIx(Ml{NhMEzt!^ZU{@-7AhJX3z|L=<+Y0K?aXwqpcf^?dp z*moM_%q?3?S`UjRXHU{SkJ%U|u{xaKuE++WQXISGP7U8!?gr;g>m)0=Z5pE zj$TMS`uxR4)9L&WOn6*^>1I+Rn|a-%Ru1jn&V0l5RQI00#;f*MPlA?s8^-;=dEfOS z<2F607`D4J-)Ljj=icG%e!BPnd5)5JLRmb+(^+i7sPr9nD?|Y-+5g|&+j&vH|84KZ zWBkv(JUvEoPSY3?fy+eDS|VVTr`)@lK_D*lZ3QjMZaGbX1*MnWIwOHvu{)beYmNv~ zVD&8&jUXTcRGU##MqMID(*q*#fo+)jRHY7X{Neaq@ z5-}uc%ut@;1ae2d+Ag>*VO575AW4CRpyXi&-SbGSAVkhvDFGU{IiA!d`8Oi@=7y}V zXpZXXS*whbH&4-NTVi@|bzq-9ZCKVwsTGSH?Spfg$zkwTEqiSMz}(;ac17``U{Y*$ z{Jp$ewrbB=vh)Iw_uQ=?Cu2+_P`M*tOC#0iy0Vb2&<%Y+e}+z3p~ZLJMIJ-<*29W0 z4IkRlLRR>9?+*W(wX)81vP9F>T#qU>Q2A#p+t}%dIC@-XCRB*;U#ItKaHqFpKiXSW z57^X8oByGn95(X0zJxA^7^>&vY(n;3qK#Vr>(vhx9p}}G+{ChtjI}TQ(xdW26TzLD zH_!75s-D{?qza(H}talAep-Rr-5FLs~S&VTPd z+j(67y`Kjxdk%7#%n*2m6GS-L=qPDp)AmL?Ti|~|Qb4uuND!#44UN4W&UtfTO(<5;B=Je>~^5o6w1^DjGIe2?YeA$PkDT*QiYd+ZKQfIpJ(zyx5Yhk#BXXuY}I$l-L+M zT~J#`1xF(sf$u2O*X$N7ryTK$t7V7VK8g&KNHv9nO+3(5=`~_oAjJ$N0;r_y`fqhh zNCFFvWH|#xe^MM^ZfrH30A3_BL)aF$o*~uz@j_D%6>OI?*H}suWndHIf$knY!?}bs z!4t6nIbw+fz4?6SOLd+5oHAq>Zvhp83rJGg2Yd#ZbQ&~7JOE=v&;%zKW&Z^7* zPKyX^Ql|c5(ZKC>NEA=5a9T)M47l=P0HF7XC74S>+{+=9oasTJx}_m%7itGKQZWN{ zNy}BJHPfkq=K3lC_)dkM(Ns<;bdDX{!P=*hpT~tz-kS>5N2$7^f=6t6NmI7M5) zi|jREbb&29{+$@&iMp=4B>m0!Unmhx(jn0y5P%_GWNLiqs~S);g9Ing4iLd0;W}8t z2$Xu0nV%B?fo6{ax8*m`z^FA-y0KX0WK_hNW=;`N={8fp!LIhwaed{D9(wG~QHr7R z{hVh1Oxe4JQn;q(MwxECG|3iF{LQM zQZHETMyS9ED%G2DZ&Qg|sudxmZBZPu)EW?*kN^`JuFnaoJW8p(1w`KogWv1xGzJwH zju69FP~a;BBtv*qmjH<)%mdSU14;A15?&rZkV7$*IHdG5I@h{NqEg+Z?!;0lvMObO zQk6*;W`Ln9th!Q4F`>eY6M!-}rp*4L%yzRonhY>$<ho-tgZ7b@mzV>11DeYI3dKE5zErtjt}fwQwC3sg)IaDeq%6gV_YoNxSpX&)S2iY*SQ*+e>?2s%1I>YRj8Zf|D)T z3uBlmIl5-D9#PImK@9hS90e}ZQN<|9C&8=1qWI6ZI!#fjyIdTUI5dGd#ByG(u1err zuE{E0A)WP?yDA)WULa{g5@jPz3ElnD@@a3S^tD{3Z#injlLCj^l5Z*INx_wsR~Y9? zUm7>^XQlBf(}dnzCirc&@|aM;o+$UAxFOz zh=@$0O(@H$HrLYOb%vi-M0+Ffy>zQ2+QYK>*0TZ^g*L86R)donXPCKK8Np-*90mcY zLdFa2#VNnw@3a6=dU!b!1+gvxl)WynKZeR?UbER8!2R(H<=nP7w`v>TqO_TB#a-go)GAb1)1*uG{ML z-Wc^ztl0Y937|6Y1wsz)_ZMnSb#-r%dRMDfNJHl#N%Y)QoMJy3b7Yk;Mx0 zJd@K+O0tFKveaFMv?POg&P`dTapQ#s;&Qjtn*=3@bI7n7ofC%1)Xr!Swzkh5_?vtH zV5V-!G!wx27?aYgSJmnjY=iU+*H~LYjg#jSXHZ{p<)kAsTrDTBLGE7eB~3= zv|^WnQ#L^p&dpT2f|FF8v#`yw{V`Xbs+b||iusagT_!sjH6z7NG^#OXOrG_sv~`d} zRQ8^g709i_mO@bqU&&p})jToPs^HlOoFjK-Xrxfj;iA%PbuE<899!SU(?=a%p3PD% zXKclMOUn_}w-?gVBKy;3ZNghL*;X|bQVgj?YmSiaG=jj3C^+3((Smue- za)edZamz^emQ;%=l77>c<4h}ZH}01rn(+N zjW{MM!v%~7UvH?-v~(bqR@wh%;wQU0I(>6_a(Em8wJ9d~Er*@K0rW9f`?#aaQ2~LG z(%`J>F9(#JdP2YuOw}B-5;_!Qt36%dvfn5*r5SWXR1>!P@YMkm=5Ua&O4y(P8G>9+ zr`%O~Q>7ZM>P9KPZ^?v~#){V!hdim`oizNntIRzibH}y600r=*(lgRzPAd!8giqO4 zlgrThdd@1BnHvPSG^vdg?e=|AXaV64$Vo5g~>N66yH1|19 zJ<(C~DM%zzMABkz-BwRxtYXpgKii#kou(MgKvzk{C(Acf=tZ?adCv>h|X(>nEvP?Nssm%nu4tcI7J7y<9&s8fg zkdMGyk|EAjr=jFS?3F6k zk4`DlH#aDk+u0G4j5ix<+I_Lx7;uibIw`bzY5}e_t1zmKqiSQ}^5pW>@fJ9J zb2>aZ{qFqa^!wx2$ETND;PvtO;SUF=mj~aTygIr3yAq-APA*T6FLe08fq`*$aDI7m z`1aMoIXHWJe)i_#SX*1YTbQ9t&eeEM3CC*JLG8@wS)ng3FwYsy8J6z3YVZUUYOPes zU!`%K^-{gs$ayhWa}}$uF;|+M(*&2(WUbWAKC4=dbvIg@lUXf$-;BVklBKfJD{Rki zf)i=ZLZ*}_?Bu{&9UGT|;65H!v36p;iCw#wb^EpI8eT=`il z@0;3D;2=dA9xI1V5jSO&^K#$9q7(oof~(!}@c7W$(=#8GfwAqf3@gNDML=~eoWrTV zhA(T|kbo*UK;4;NEwW*f;8Z%cdMiOXFnUcF!^{G;8akOl$s)u6WO~nBTJh3W^CA=V ziLT;iQRKP!t3%wBSw6%rQ}fE8(mw7Gcd}sSat6iEGwHhf!?nIy*ty+iz0hPvsa^$DE1lkE zyIOMv5KNHLCtCn23L#05ZbYt^L5=oXD7iy(f<-wV%iY_|(hlf2GpkU_Z`qb=M|!?` zs|9mWo^z3s${B!_U?NbpgBy)B{N}tO6h^I9s>N{uRd zw`_#1E?#N$YNf~#1G6S9=S+B_^|KuCdb}&%OiJFQXo5(pYs_et2A0ktn=4IZo$Hc| z)o?Btt9HxG$}!{|F*$t9BI;Ii-FUn(&PLUsh2%iR)6$v0cBIj98%tWW03V+oNxK>f zgHzuR&d!cck52w)Uv{BdEy(k1VZs;O&^`H0k#b$`B>?~~dsf&oVHEyqf_1AY&JfeF z%z8GrRn6ol7-cC3h$I>1TIr7&Ox_{ELG;T%qiT|p!Ngj-g%u{HRn4r=nMsep<`E@- zF5?B9F=~PTX#mu0T}@N@j22lcUHp;`Gdpn1wzD^@BE}a)!1v`wsG8Jk0!H9x1YpJ~ zU`UrX>!enTtFl^d>;(~O+NIpg+!|xMwK_&s$cow_wUlsKF;W>uQ3`UE(HrvG&Zh}} zkz^vsak(FFIA`}=%jKDBkpr@1hOexqtTq!R*N(5$3hnWGsfwhj(qRp%q>+!*nJ)Q~ zv*pHuf=yYV47VKow*+CQB2ouv%Vk#1tp|^ZnSm;;SPE0=-8d6|I#vs1&|9suG7N>4 z^kvbeChU2*H`-BE`Z(upZifjs+i({sJ$_m8F!u_yIp4SRysgiz*mi-CM}n1aZcgfe z43cR9rwB~x6=I|w)Mpk-Dp#F1+ckQO=>MHOf%yOP~i@B_-`Aj4w@b@DE7WJLm)Ot%{wPo98FrS|BgTK(Sm*n9~-ZhRaL zhaWdSg0H?h$Z=^14oIr<4S)5OECPNhPaeI)B#n`}kMf_JPeQK;wHi|-VwmID1klCf zg(~gVZYiQ6ef8C`UJ5$m;P?uwsY`>boT-^{obni%GI146>Eh}xS)sh*DuV5d(f5l6 zIz+DY2{DzM$_4L#vEY8d(-}B$@-a0a`e(shi2j#4cdL_NZk_6_rQpY6jKKLACM_+h zr`t+koFHZ#5|5tlk`~`x1ni?Xdu4Ow*$&V{}_r43o* zmX$c8QyxpVB}VTBg8}4A>(qOuwVs?7smi^ul%;Ep{oZV#fkSopLS^=DYp>mGi}gW` zSbDet@w%6H=6C}Bu+Gsr1Z6N!^~$4JBSV*sdn z-$Io7784pzAkNzA+V3qej0Cr#cR%-l&_PV7SZBLZJVjizHcNl*0bwbF{9^S^(_#qJ z4Tr6rXJl1Pi$i$a?=1)}2+ngPmg8^_@5Ef!>cSZ+-=F~tP$s2srvM8l?_@Hg*CZx% zOw&btMYAF}D3(`h>%vgQ%Bx=uVZB~ooG5u0XP6Z4<9CP=6qKQ5b!xoU8R*)0tP{@# z(C7~}SZ6Be7aY+!#7$Sv)YP%UajFUhAba0y2rf`ks2f2K^-2C#95j`Ruh3VU>asB_ zNDO&g@G=!p2z$SpEgiPBwwAP;;MFy6hgUm2*9;43zv4-#V{Z!Trj(UK;U=_{m2ZuG zEv#vW)@rNlIPD2H=R9VJ3x*RBbXIN&@uqxal!lakuX-!u6nKu}6ir}}t%B06wt8<6 zDq@&r^g7NNzQP%rq8RZ6W{qj;ZDHL&QV>k_Y(sK0R&lfHRFalLj+FLaQC51vTqCV( zB5!qy%05_eG*$Ih$7y;Sr7>Ud7^ZVfZcIlvtlRKoU7ghj;yR^1fw^9+!HCBxqq!Vy z>k|JU@H?koWjKQAstW&RNLFvn8fm^a%6fT&Yl3|D@^LG_;8wWm9=f^xxO0ALm&nxo z(^OJ9x_LW(jaX&cPWI+i!5G)2H>d2cT66M-0knVCitnxfdInlIDOd29ZC zD%9KQm2#nZAL_LP%@Au28|SsD5trYRj23B3Sv*^e8BXnb=|4B0EG<*VUTdg>wGUrH z#T(Q@vd|pxTiRWq0p%KJX(9(z8ljnA*Z?rzcdP0mUfjHWb64*>2M?*|FA{+~Rg$4jSDU+%=T;tCHh53oj3%#-NYcS8TLr#_C zIQpx6Jyci3N9xvxUk$7SHdSFBd}BkF-QO63RdO%O;_7N`6~~)XrmWFMjYTaXM-IDG zNI4PC<=%j++ibV0#BWt1JP8=M6Wy&UC6hhqsX0ujP&oqRFRsrm=@SIflJ;R|3+bPc zwNIXaW{g&i;oWgp8ynytO*#10$kh-Qg7O4rh>iT~rF785Fy%uF=2s_gp3Y+Fq!hTT-0Ay9oo%(G&ENf)bHsxk&Z(XWY>qw zK65Ir*2l$)BSLmc!`9aN#gE`~RE)lBEm|NRNyTi%2XipP(^8s7+h z4L5#i80*Fcc)s%**Ob331b)rvbVzf-M|1dosE+@91-y40{~P@ZuI=G1n5O!)O%A#A zn2ai@#=9kEXovw4fH~qi)wwKyq6%n|n(N%+VOD31UN$Ef3sX zCr26jtw3B#elISn6POCqtvFXUWvynjBak`F<4}CEJHJuq?BthI<(^u1!KQYxQ?CMYD!ZYH1McTnbkOFGVflM#Jm&Q~<);mQ+>O@`bF&J_E_rB9kSKMhKY>U( zc9GO@>M!2U*@}OSICh4(Y+7=)`=qxlKuhD1dXv4BVba#SUx{LCw zo|CQ8JN*#eOV0(O;+=-d8MAf`^#oikVr1N&64n(=G9evxe6wB-P;pP49uC@fd_x_6 zui^hCoT&d>zEzk$4lc@=a`LF61;h&9GL z^jGDt3*AwAfy6ekqw5_4y|t%$4{L}kbISo=G3Xwc5@bVVmeiS%Fk7(xRI)<-Ub*$E zmTP}Y$IC2>f#|QgsQ$faFEbsiofYK)1lx%AX4}IW;pXEBn9zcxEAzpE?c&GhX|;QL zCfsP1uuvE4u;6XYci}(m)^~S(fD1Q~h7iJs#}@baQiC1(_3A@F*Zcix1pgFQ8#`{U z8hrQF*Qv3KpgapOk3fA6Ol5CNJ1(Z&vn77bDZxlOJ$ znj8!}J)j7RKv~f-8!e?uT3;5p4_|PGFFjYQ7kC)`Z{QR)xyR^vl?OD(Uli&N7j(z?8WU%L~fjxj{x`zuHx6D}+G^9)$~ z!GwKC?~4jtEI_^ZPJGsB%1%$0r_YzqPoAH=fJ?r~ZZ)~i4Nr4Ul<>G)D5?^+CGxPv z*Og%pR2{mgpx~4->oQw^xyn-EuMR1loI}y7F)CJqK5co+(;b#Ul)dK|idy^CAm9FKD|7;^EDF=#3q28L#g=3WT+n#k~go_9&EXr)Y=wG)rq z33GsKZh4_;pJO4X4afVG_MkZX8~kbc*X8M7W|*IJnEjsm0uO?W%O{o?we2 z`rzyTC2|UWwE`c&a%(fNxCf?j!NDGY7g=INYsqeToA&81GEK#&mkyL1L&dtyCt*5H zJBRk^$;k=+Cp66eQZ}f;ECTpDnxXzU5K|g;4#TtoCL>vsmcmEsH}h)ip;6gkVZ)nB zI!B((5_hmSn6tOlPJs#qp*kxZ($ zBhk5Uk14XO3gR0lK^wbL>z<(T=^GA1rsQkY{2(tfJg;#{LSK{25IFfd-k|_fFJK?h;4wSsVID#lz4FBiB3Hy`d;MgRVk?Pf+DZw8loNP_lMYjv1hSWoCc5qm z(+`{C0--T%$PoyQp;8Hn)7%a=#-0b*VRaK@3;;iQ$iQ5rULl|g$<4XxIme1l(|$8` zV3=aU4=I!%)>H%b8rjC2l&)3Pz0{B~oPk@D#{aW_p1*y|fL;Cj>f^=x%U7#6;TNIb zs)DlXCJa;SdY&4++iLqAR?gr8_#EE%)JrZ%6W;UJvsd4PUr3*g;pyPoeE3U2N8vKZ z!H^n#R(C!35@NNY0kklb<%QZ#La(BNRltl7gUF&TBv;^qBo?T>=)SvUScNHJ9c;~b4)GvTjja8 z4ZS$KGgo)v7kE&q;5EY{pem|&11k?ucaF8?3QdzoSD)wm?tus&80Y!Tk~<;3TO|9j z86-&bF-!itEi2Fu=wRR~19}mY#22@53X>rkRKSBy-w6)fdo14?CM4*xspqJ1LpER> zMfA8>z|R!lj|cM1eK7Zz&{_km3Bous6H4a`Q1$#vR(wgbI4L!~2N2JV6&wmABl9U_ z5Mb>JO9$lShuy=L5xq}VQlgQw4wartm zhO(ND)lF2R$kP|IZDnG#Bf|_B{BYW*xa0?*-HmYskm`xQcOYC#7rb0zM%pB?1=8a} zE2%@j2od&Fd3!;JocBMkz+y{N&3ltXE12NxPyvAutv#le-v77Yq2omv56JZ{<2Gl1%c1 zyj@6RPMG#xKa)I6fZ9?L)9&gAI1ZynDP>z~pHkgys{7PZc53zsmhc1vFq5SpTKfWg zCyHg#|DbWODi)(@SbEI$%Iq_^i}p2yxvu*NV22gfXGNQMHiQiUYeI|a(|-)fE%9?T zy3`%8#=sG|PyE$S=YOQw1myT3dY>wz-UHTBfITZ-B7Z}&O^K%tI|7OuS*_lIEkGyd zo^9Bhh)<24w+|onkQ+UHd`hwy)`h6V8bh`$90-Dp3LYmr-B_|h|AX=&WA^23!Gpq4 zS9T&CO%mZgL|UH|lR&Y4lN6s4Ffx|Jhu0qTcQ)MX(+>10B^<(JW56@1!CfaE64}8| zsnN~t+4dW zO^_7gma94ZXQ2jaa#~RZZi;BnM2v=rpZk!*cOvsn^GmoBPs8;_-$OO*R;zbGaTwIj zC)VoZvu;jVn;BdlyA_L)-n7+g-6o_NJPh6CJ3bajgZEs_%1Asm3^_!uIQ879d_a1#0D;!3WE@ z1%|;(^L&;2m&<;>c;z^J>xO*bU*k=lhGB=%lrPUjSvBK#Tl6rh2^3kHr?KRbHQNF7 z1e?H{UaMPk`w86VVX9v99UtBVx|L%Ct)SP_f1@=#rV0(Ud{?F2<}uP#_tlm94yKhq zDEKumxEx6kIH-yBRxLWtg${~6v)8@|nYh1@4Z;3pw~5aNs(SyD4+(p9ac)C$y~{|! z?EU?d*~oa`(7|nSe8X+1Cv(7JVPfbt@8T^la6NmSZvmgjm>&E5;Z3JU0R}H^QD@yJ zHOJ>A=(UEPb_N-e$^&ibES$khznC6l1~VYk_4ivNEz34u9A&^@fpcw-uJ`9+Xw{WD z9ecFJBMJ}t&Dft!qWS&UlPALelFSk?cd1_=^*!7pcqn8(_|dm_akaiNaq+m7nkBDf zCCr`<-E!M1%{DVR!9|T-z`W}wSZd@mZ=gOmu|KOfr?4{#RF:`. + type: string + deleted: + description: Deleted is when the release was deleted. + format: date-time + type: string + digest: + description: |- + Digest is the checksum of the release object in storage. + It has the format of `:`. + type: string + firstDeployed: + description: FirstDeployed is when the release was first deployed. + format: date-time + type: string + lastDeployed: + description: LastDeployed is when the release was last deployed. + format: date-time + type: string + name: + description: Name is the name of the release. + type: string + namespace: + description: Namespace is the namespace the release is deployed + to. + type: string + ociDigest: + description: OCIDigest is the digest of the OCI artifact associated + with the release. + type: string + status: + description: Status is the current state of the release. + type: string + testHooks: + additionalProperties: + description: |- + TestHookStatus holds the status information for a test hook as observed + to be run by the controller. + properties: + lastCompleted: + description: LastCompleted is the time the test hook last + completed. + format: date-time + type: string + lastStarted: + description: LastStarted is the time the test hook was + last started. + format: date-time + type: string + phase: + description: Phase the test hook was observed to be in. + type: string + type: object + description: |- + TestHooks is the list of test hooks for the release as observed to be + run by the controller. + type: object + version: + description: Version is the version of the release object in + storage. + type: integer + required: + - chartName + - chartVersion + - configDigest + - digest + - firstDeployed + - lastDeployed + - name + - namespace + - status + - version + type: object + type: array + installFailures: + description: |- + InstallFailures is the install failure count against the latest desired + state. It is reset after a successful reconciliation. + format: int64 + type: integer + lastAttemptedConfigDigest: + description: |- + LastAttemptedConfigDigest is the digest for the config (better known as + "values") of the last reconciliation attempt. + type: string + lastAttemptedGeneration: + description: |- + LastAttemptedGeneration is the last generation the controller attempted + to reconcile. + format: int64 + type: integer + lastAttemptedReleaseAction: + description: |- + LastAttemptedReleaseAction is the last release action performed for this + HelmRelease. It is used to determine the active retry or remediation + strategy. + enum: + - install + - upgrade + type: string + lastAttemptedReleaseActionDuration: + description: |- + LastAttemptedReleaseActionDuration is the duration of the last + release action performed for this HelmRelease. + type: string + lastAttemptedRevision: + description: |- + LastAttemptedRevision is the Source revision of the last reconciliation + attempt. For OCIRepository sources, the 12 first characters of the digest are + appended to the chart version e.g. "1.2.3+1234567890ab". + type: string + lastAttemptedRevisionDigest: + description: |- + LastAttemptedRevisionDigest is the digest of the last reconciliation attempt. + This is only set for OCIRepository sources. + type: string + lastAttemptedValuesChecksum: + description: |- + LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last + reconciliation attempt. + + Deprecated: Use LastAttemptedConfigDigest instead. + type: string + lastHandledForceAt: + description: |- + LastHandledForceAt holds the value of the most recent + force request value, so a change of the annotation value + can be detected. + type: string + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + lastHandledResetAt: + description: |- + LastHandledResetAt holds the value of the most recent reset request + value, so a change of the annotation value can be detected. + type: string + lastReleaseRevision: + description: |- + LastReleaseRevision is the revision of the last successful Helm release. + + Deprecated: Use History instead. + type: integer + observedCommonMetadataDigest: + description: |- + ObservedCommonMetadataDigest is the digest for the common metadata of + the last successful reconciliation attempt. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + observedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string + storageNamespace: + description: |- + StorageNamespace is the namespace of the Helm release storage for the + current release. + maxLength: 63 + minLength: 1 + type: string + upgradeFailures: + description: |- + UpgradeFailures is the upgrade failure count against the latest desired + state. It is reset after a successful reconciliation. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + deprecated: true + deprecationWarning: v2beta2 HelmRelease is deprecated, upgrade to v2 + name: v2beta2 + schema: + openAPIV3Schema: + description: HelmRelease is the Schema for the helmreleases API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HelmReleaseSpec defines the desired state of a Helm release. + properties: + chart: + description: |- + Chart defines the template of the v1beta2.HelmChart that should be created + for this HelmRelease. + properties: + metadata: + description: ObjectMeta holds the template for metadata like labels + and annotations. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations is an unstructured key value map stored with a resource that may be + set by external tools to store and retrieve arbitrary metadata. They are not + queryable and should be preserved when modifying objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + type: object + labels: + additionalProperties: + type: string + description: |- + Map of string keys and values that can be used to organize and categorize + (scope and select) objects. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + type: object + type: object + spec: + description: Spec holds the template for the v1beta2.HelmChartSpec + for this HelmRelease. + properties: + chart: + description: The name or path the Helm chart is available + at in the SourceRef. + maxLength: 2048 + minLength: 1 + type: string + ignoreMissingValuesFiles: + description: IgnoreMissingValuesFiles controls whether to + silently ignore missing values files rather than failing. + type: boolean + interval: + description: |- + Interval at which to check the v1.Source for updates. Defaults to + 'HelmReleaseSpec.Interval'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + reconcileStrategy: + default: ChartVersion + description: |- + Determines what enables the creation of a new artifact. Valid values are + ('ChartVersion', 'Revision'). + See the documentation of the values for an explanation on their behavior. + Defaults to ChartVersion when omitted. + enum: + - ChartVersion + - Revision + type: string + sourceRef: + description: The name and namespace of the v1.Source the chart + is available at. + properties: + apiVersion: + description: APIVersion of the referent. + type: string + kind: + description: Kind of the referent. + enum: + - InternalNelmOperatorHelmRepository + - InternalNelmOperatorGitRepository + - InternalNelmOperatorBucket + type: string + name: + description: Name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: Namespace of the referent. + maxLength: 63 + minLength: 1 + type: string + required: + - kind + - name + type: object + valuesFile: + description: |- + Alternative values file to use as the default chart values, expected to + be a relative path in the SourceRef. Deprecated in favor of ValuesFiles, + for backwards compatibility the file defined here is merged before the + ValuesFiles items. Ignored when omitted. + type: string + valuesFiles: + description: |- + Alternative list of values files to use as the chart values (values.yaml + is not included by default), expected to be a relative path in the SourceRef. + Values files are merged in the order of this list with the last file overriding + the first. Ignored when omitted. + items: + type: string + type: array + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + This field is only supported for OCI sources. + Chart dependencies, which are not bundled in the umbrella chart artifact, + are not verified. + properties: + provider: + default: cosign + description: Provider specifies the technology used to + sign the OCI Helm chart. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + version: + default: '*' + description: |- + Version semver expression, ignored for charts from v1beta2.GitRepository and + v1beta2.Bucket sources. Defaults to latest when omitted. + type: string + required: + - chart + - sourceRef + type: object + required: + - spec + type: object + chartRef: + description: |- + ChartRef holds a reference to a source controller resource containing the + Helm chart artifact. + + Note: this field is provisional to the v2 API, and not actively used + by v2beta2 HelmReleases. + properties: + apiVersion: + description: APIVersion of the referent. + type: string + kind: + description: Kind of the referent. + enum: + - InternalNelmOperatorOCIRepository + - InternalNelmOperatorHelmChart + type: string + name: + description: Name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace of the referent, defaults to the namespace of the Kubernetes + resource object that contains the reference. + maxLength: 63 + minLength: 1 + type: string + required: + - kind + - name + type: object + dependsOn: + description: |- + DependsOn may contain a meta.NamespacedObjectReference slice with + references to HelmRelease resources that must be ready before this HelmRelease + can be reconciled. + items: + description: |- + NamespacedObjectReference contains enough information to locate the referenced Kubernetes resource object in any + namespace. + properties: + name: + description: Name of the referent. + type: string + namespace: + description: Namespace of the referent, when not specified it + acts as LocalObjectReference. + type: string + required: + - name + type: object + type: array + driftDetection: + description: |- + DriftDetection holds the configuration for detecting and handling + differences between the manifest in the Helm storage and the resources + currently existing in the cluster. + properties: + ignore: + description: |- + Ignore contains a list of rules for specifying which changes to ignore + during diffing. + items: + description: |- + IgnoreRule defines a rule to selectively disregard specific changes during + the drift detection process. + properties: + paths: + description: |- + Paths is a list of JSON Pointer (RFC 6901) paths to be excluded from + consideration in a Kubernetes object. + items: + type: string + type: array + target: + description: |- + Target is a selector for specifying Kubernetes objects to which this + rule applies. + If Target is not set, the Paths will be ignored for all Kubernetes + objects within the manifest of the Helm release. + properties: + annotationSelector: + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource annotations. + type: string + group: + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + kind: + description: |- + Kind of the API Group to select resources from. + Together with Group and Version it is capable of unambiguously + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource labels. + type: string + name: + description: Name to match resources with. + type: string + namespace: + description: Namespace to select resources from. + type: string + version: + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + type: object + required: + - paths + type: object + type: array + mode: + description: |- + Mode defines how differences should be handled between the Helm manifest + and the manifest currently applied to the cluster. + If not explicitly set, it defaults to DiffModeDisabled. + enum: + - enabled + - warn + - disabled + type: string + type: object + install: + description: Install holds the configuration for Helm install actions + for this HelmRelease. + properties: + crds: + description: |- + CRDs upgrade CRDs from the Helm Chart's crds directory according + to the CRD upgrade policy provided here. Valid values are `Skip`, + `Create` or `CreateReplace`. Default is `Create` and if omitted + CRDs are installed but not updated. + + Skip: do neither install nor replace (update) any CRDs. + + Create: new CRDs are created, existing CRDs are neither updated nor deleted. + + CreateReplace: new CRDs are created, existing CRDs are updated (replaced) + but not deleted. + + By default, CRDs are applied (installed) during Helm install action. + With this option users can opt in to CRD replace existing CRDs on Helm + install actions, which is not (yet) natively supported by Helm. + https://helm.sh/docs/chart_best_practices/custom_resource_definitions. + enum: + - Skip + - Create + - CreateReplace + type: string + createNamespace: + description: |- + CreateNamespace tells the Helm install action to create the + HelmReleaseSpec.TargetNamespace if it does not exist yet. + On uninstall, the namespace will not be garbage collected. + type: boolean + disableHooks: + description: DisableHooks prevents hooks from running during the + Helm install action. + type: boolean + disableOpenAPIValidation: + description: |- + DisableOpenAPIValidation prevents the Helm install action from validating + rendered templates against the Kubernetes OpenAPI Schema. + type: boolean + disableWait: + description: |- + DisableWait disables the waiting for resources to be ready after a Helm + install has been performed. + type: boolean + disableWaitForJobs: + description: |- + DisableWaitForJobs disables waiting for jobs to complete after a Helm + install has been performed. + type: boolean + remediation: + description: |- + Remediation holds the remediation configuration for when the Helm install + action for the HelmRelease fails. The default is to not perform any action. + properties: + ignoreTestFailures: + description: |- + IgnoreTestFailures tells the controller to skip remediation when the Helm + tests are run after an install action but fail. Defaults to + 'Test.IgnoreFailures'. + type: boolean + remediateLastFailure: + description: |- + RemediateLastFailure tells the controller to remediate the last failure, when + no retries remain. Defaults to 'false'. + type: boolean + retries: + description: |- + Retries is the number of retries that should be attempted on failures before + bailing. Remediation, using an uninstall, is performed between each attempt. + Defaults to '0', a negative integer equals to unlimited retries. + type: integer + type: object + replace: + description: |- + Replace tells the Helm install action to re-use the 'ReleaseName', but only + if that name is a deleted release which remains in the history. + type: boolean + skipCRDs: + description: |- + SkipCRDs tells the Helm install action to not install any CRDs. By default, + CRDs are installed if not already present. + + Deprecated use CRD policy (`crds`) attribute with value `Skip` instead. + type: boolean + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation (like + Jobs for hooks) during the performance of a Helm install action. Defaults to + 'HelmReleaseSpec.Timeout'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + type: object + interval: + description: Interval at which to reconcile the Helm release. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + kubeConfig: + description: |- + KubeConfig for reconciling the HelmRelease on a remote cluster. + When used in combination with HelmReleaseSpec.ServiceAccountName, + forces the controller to act on behalf of that Service Account at the + target cluster. + If the --default-service-account flag is set, its value will be used as + a controller level fallback for when HelmReleaseSpec.ServiceAccountName + is empty. + properties: + configMapRef: + description: |- + ConfigMapRef holds an optional name of a ConfigMap that contains + the following keys: + + - `provider`: the provider to use. One of `aws`, `azure`, `gcp`, or + `generic`. Required. + - `cluster`: the fully qualified resource name of the Kubernetes + cluster in the cloud provider API. Not used by the `generic` + provider. Required when one of `address` or `ca.crt` is not set. + - `address`: the address of the Kubernetes API server. Required + for `generic`. For the other providers, if not specified, the + first address in the cluster resource will be used, and if + specified, it must match one of the addresses in the cluster + resource. + If audiences is not set, will be used as the audience for the + `generic` provider. + - `ca.crt`: the optional PEM-encoded CA certificate for the + Kubernetes API server. If not set, the controller will use the + CA certificate from the cluster resource. + - `audiences`: the optional audiences as a list of + line-break-separated strings for the Kubernetes ServiceAccount + token. Defaults to the `address` for the `generic` provider, or + to specific values for the other providers depending on the + provider. + - `serviceAccountName`: the optional name of the Kubernetes + ServiceAccount in the same namespace that should be used + for authentication. If not specified, the controller + ServiceAccount will be used. + + Mutually exclusive with SecretRef. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + secretRef: + description: |- + SecretRef holds an optional name of a secret that contains a key with + the kubeconfig file as the value. If no key is set, the key will default + to 'value'. Mutually exclusive with ConfigMapRef. + It is recommended that the kubeconfig is self-contained, and the secret + is regularly updated if credentials such as a cloud-access-token expire. + Cloud specific `cmd-path` auth helpers will not function without adding + binaries and credentials to the Pod that is responsible for reconciling + Kubernetes resources. Supported only for the generic provider. + properties: + key: + description: Key in the Secret, when not specified an implementation-specific + default key is used. + type: string + name: + description: Name of the Secret. + type: string + required: + - name + type: object + type: object + x-kubernetes-validations: + - message: exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef + must be specified + rule: has(self.configMapRef) || has(self.secretRef) + - message: exactly one of spec.kubeConfig.configMapRef or spec.kubeConfig.secretRef + must be specified + rule: '!has(self.configMapRef) || !has(self.secretRef)' + maxHistory: + description: |- + MaxHistory is the number of revisions saved by Helm for this HelmRelease. + Use '0' for an unlimited number of revisions; defaults to '5'. + type: integer + persistentClient: + description: |- + PersistentClient tells the controller to use a persistent Kubernetes + client for this release. When enabled, the client will be reused for the + duration of the reconciliation, instead of being created and destroyed + for each (step of a) Helm action. + + This can improve performance, but may cause issues with some Helm charts + that for example do create Custom Resource Definitions during installation + outside Helm's CRD lifecycle hooks, which are then not observed to be + available by e.g. post-install hooks. + + If not set, it defaults to true. + type: boolean + postRenderers: + description: |- + PostRenderers holds an array of Helm PostRenderers, which will be applied in order + of their definition. + items: + description: PostRenderer contains a Helm PostRenderer specification. + properties: + kustomize: + description: Kustomization to apply as PostRenderer. + properties: + images: + description: |- + Images is a list of (image name, new name, new tag or digest) + for changing image names, tags or digests. This can also be achieved with a + patch, but this operator is simpler to specify. + items: + description: Image contains an image name, a new name, + a new tag or digest, which will replace the original + name and tag. + properties: + digest: + description: |- + Digest is the value used to replace the original image tag. + If digest is present NewTag value is ignored. + type: string + name: + description: Name is a tag-less image name. + type: string + newName: + description: NewName is the value used to replace + the original name. + type: string + newTag: + description: NewTag is the value used to replace the + original tag. + type: string + required: + - name + type: object + type: array + patches: + description: |- + Strategic merge and JSON patches, defined as inline YAML objects, + capable of targeting objects based on kind, label and annotation selectors. + items: + description: |- + Patch contains an inline StrategicMerge or JSON6902 patch, and the target the patch should + be applied to. + properties: + patch: + description: |- + Patch contains an inline StrategicMerge patch or an inline JSON6902 patch with + an array of operation objects. + type: string + target: + description: Target points to the resources that the + patch document should be applied to. + properties: + annotationSelector: + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource annotations. + type: string + group: + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + kind: + description: |- + Kind of the API Group to select resources from. + Together with Group and Version it is capable of unambiguously + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource labels. + type: string + name: + description: Name to match resources with. + type: string + namespace: + description: Namespace to select resources from. + type: string + version: + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + type: object + required: + - patch + type: object + type: array + patchesJson6902: + description: |- + JSON 6902 patches, defined as inline YAML objects. + + Deprecated: use Patches instead. + items: + description: JSON6902Patch contains a JSON6902 patch and + the target the patch should be applied to. + properties: + patch: + description: Patch contains the JSON6902 patch document + with an array of operation objects. + items: + description: |- + JSON6902 is a JSON6902 operation object. + https://datatracker.ietf.org/doc/html/rfc6902#section-4 + properties: + from: + description: |- + From contains a JSON-pointer value that references a location within the target document where the operation is + performed. The meaning of the value depends on the value of Op, and is NOT taken into account by all operations. + type: string + op: + description: |- + Op indicates the operation to perform. Its value MUST be one of "add", "remove", "replace", "move", "copy", or + "test". + https://datatracker.ietf.org/doc/html/rfc6902#section-4 + enum: + - test + - remove + - add + - replace + - move + - copy + type: string + path: + description: |- + Path contains the JSON-pointer value that references a location within the target document where the operation + is performed. The meaning of the value depends on the value of Op. + type: string + value: + description: |- + Value contains a valid JSON structure. The meaning of the value depends on the value of Op, and is NOT taken into + account by all operations. + x-kubernetes-preserve-unknown-fields: true + required: + - op + - path + type: object + type: array + target: + description: Target points to the resources that the + patch document should be applied to. + properties: + annotationSelector: + description: |- + AnnotationSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource annotations. + type: string + group: + description: |- + Group is the API group to select resources from. + Together with Version and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + kind: + description: |- + Kind of the API Group to select resources from. + Together with Group and Version it is capable of unambiguously + identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + labelSelector: + description: |- + LabelSelector is a string that follows the label selection expression + https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#api + It matches with the resource labels. + type: string + name: + description: Name to match resources with. + type: string + namespace: + description: Namespace to select resources from. + type: string + version: + description: |- + Version of the API Group to select resources from. + Together with Group and Kind it is capable of unambiguously identifying and/or selecting resources. + https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/api-group.md + type: string + type: object + required: + - patch + - target + type: object + type: array + patchesStrategicMerge: + description: |- + Strategic merge patches, defined as inline YAML objects. + + Deprecated: use Patches instead. + items: + x-kubernetes-preserve-unknown-fields: true + type: array + type: object + type: object + type: array + releaseName: + description: |- + ReleaseName used for the Helm release. Defaults to a composition of + '[TargetNamespace-]Name'. + maxLength: 53 + minLength: 1 + type: string + rollback: + description: Rollback holds the configuration for Helm rollback actions + for this HelmRelease. + properties: + cleanupOnFail: + description: |- + CleanupOnFail allows deletion of new resources created during the Helm + rollback action when it fails. + type: boolean + disableHooks: + description: DisableHooks prevents hooks from running during the + Helm rollback action. + type: boolean + disableWait: + description: |- + DisableWait disables the waiting for resources to be ready after a Helm + rollback has been performed. + type: boolean + disableWaitForJobs: + description: |- + DisableWaitForJobs disables waiting for jobs to complete after a Helm + rollback has been performed. + type: boolean + force: + description: Force forces resource updates through a replacement + strategy. + type: boolean + recreate: + description: Recreate performs pod restarts for the resource if + applicable. + type: boolean + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation (like + Jobs for hooks) during the performance of a Helm rollback action. Defaults to + 'HelmReleaseSpec.Timeout'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + type: object + serviceAccountName: + description: |- + The name of the Kubernetes service account to impersonate + when reconciling this HelmRelease. + maxLength: 253 + minLength: 1 + type: string + storageNamespace: + description: |- + StorageNamespace used for the Helm storage. + Defaults to the namespace of the HelmRelease. + maxLength: 63 + minLength: 1 + type: string + suspend: + description: |- + Suspend tells the controller to suspend reconciliation for this HelmRelease, + it does not apply to already started reconciliations. Defaults to false. + type: boolean + targetNamespace: + description: |- + TargetNamespace to target when performing operations for the HelmRelease. + Defaults to the namespace of the HelmRelease. + maxLength: 63 + minLength: 1 + type: string + test: + description: Test holds the configuration for Helm test actions for + this HelmRelease. + properties: + enable: + description: |- + Enable enables Helm test actions for this HelmRelease after an Helm install + or upgrade action has been performed. + type: boolean + filters: + description: Filters is a list of tests to run or exclude from + running. + items: + description: Filter holds the configuration for individual Helm + test filters. + properties: + exclude: + description: Exclude specifies whether the named test should + be excluded. + type: boolean + name: + description: Name is the name of the test. + maxLength: 253 + minLength: 1 + type: string + required: + - name + type: object + type: array + ignoreFailures: + description: |- + IgnoreFailures tells the controller to skip remediation when the Helm tests + are run but fail. Can be overwritten for tests run after install or upgrade + actions in 'Install.IgnoreTestFailures' and 'Upgrade.IgnoreTestFailures'. + type: boolean + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation during + the performance of a Helm test action. Defaults to 'HelmReleaseSpec.Timeout'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + type: object + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation (like Jobs + for hooks) during the performance of a Helm action. Defaults to '5m0s'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + uninstall: + description: Uninstall holds the configuration for Helm uninstall + actions for this HelmRelease. + properties: + deletionPropagation: + default: background + description: |- + DeletionPropagation specifies the deletion propagation policy when + a Helm uninstall is performed. + enum: + - background + - foreground + - orphan + type: string + disableHooks: + description: DisableHooks prevents hooks from running during the + Helm rollback action. + type: boolean + disableWait: + description: |- + DisableWait disables waiting for all the resources to be deleted after + a Helm uninstall is performed. + type: boolean + keepHistory: + description: |- + KeepHistory tells Helm to remove all associated resources and mark the + release as deleted, but retain the release history. + type: boolean + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation (like + Jobs for hooks) during the performance of a Helm uninstall action. Defaults + to 'HelmReleaseSpec.Timeout'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + type: object + upgrade: + description: Upgrade holds the configuration for Helm upgrade actions + for this HelmRelease. + properties: + cleanupOnFail: + description: |- + CleanupOnFail allows deletion of new resources created during the Helm + upgrade action when it fails. + type: boolean + crds: + description: |- + CRDs upgrade CRDs from the Helm Chart's crds directory according + to the CRD upgrade policy provided here. Valid values are `Skip`, + `Create` or `CreateReplace`. Default is `Skip` and if omitted + CRDs are neither installed nor upgraded. + + Skip: do neither install nor replace (update) any CRDs. + + Create: new CRDs are created, existing CRDs are neither updated nor deleted. + + CreateReplace: new CRDs are created, existing CRDs are updated (replaced) + but not deleted. + + By default, CRDs are not applied during Helm upgrade action. With this + option users can opt-in to CRD upgrade, which is not (yet) natively supported by Helm. + https://helm.sh/docs/chart_best_practices/custom_resource_definitions. + enum: + - Skip + - Create + - CreateReplace + type: string + disableHooks: + description: DisableHooks prevents hooks from running during the + Helm upgrade action. + type: boolean + disableOpenAPIValidation: + description: |- + DisableOpenAPIValidation prevents the Helm upgrade action from validating + rendered templates against the Kubernetes OpenAPI Schema. + type: boolean + disableWait: + description: |- + DisableWait disables the waiting for resources to be ready after a Helm + upgrade has been performed. + type: boolean + disableWaitForJobs: + description: |- + DisableWaitForJobs disables waiting for jobs to complete after a Helm + upgrade has been performed. + type: boolean + force: + description: Force forces resource updates through a replacement + strategy. + type: boolean + preserveValues: + description: |- + PreserveValues will make Helm reuse the last release's values and merge in + overrides from 'Values'. Setting this flag makes the HelmRelease + non-declarative. + type: boolean + remediation: + description: |- + Remediation holds the remediation configuration for when the Helm upgrade + action for the HelmRelease fails. The default is to not perform any action. + properties: + ignoreTestFailures: + description: |- + IgnoreTestFailures tells the controller to skip remediation when the Helm + tests are run after an upgrade action but fail. + Defaults to 'Test.IgnoreFailures'. + type: boolean + remediateLastFailure: + description: |- + RemediateLastFailure tells the controller to remediate the last failure, when + no retries remain. Defaults to 'false' unless 'Retries' is greater than 0. + type: boolean + retries: + description: |- + Retries is the number of retries that should be attempted on failures before + bailing. Remediation, using 'Strategy', is performed between each attempt. + Defaults to '0', a negative integer equals to unlimited retries. + type: integer + strategy: + description: Strategy to use for failure remediation. Defaults + to 'rollback'. + enum: + - rollback + - uninstall + type: string + type: object + timeout: + description: |- + Timeout is the time to wait for any individual Kubernetes operation (like + Jobs for hooks) during the performance of a Helm upgrade action. Defaults to + 'HelmReleaseSpec.Timeout'. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + type: object + values: + description: Values holds the values for this Helm release. + x-kubernetes-preserve-unknown-fields: true + valuesFrom: + description: |- + ValuesFrom holds references to resources containing Helm values for this HelmRelease, + and information about how they should be merged. + items: + description: |- + ValuesReference contains a reference to a resource containing Helm values, + and optionally the key they can be found at. + properties: + kind: + description: Kind of the values referent, valid values are ('Secret', + 'ConfigMap'). + enum: + - Secret + - ConfigMap + type: string + name: + description: |- + Name of the values referent. Should reside in the same namespace as the + referring resource. + maxLength: 253 + minLength: 1 + type: string + optional: + description: |- + Optional marks this ValuesReference as optional. When set, a not found error + for the values reference is ignored, but any ValuesKey, TargetPath or + transient error will still result in a reconciliation failure. + type: boolean + targetPath: + description: |- + TargetPath is the YAML dot notation path the value should be merged at. When + set, the ValuesKey is expected to be a single flat value. Defaults to 'None', + which results in the values getting merged at the root. + maxLength: 250 + pattern: ^([a-zA-Z0-9_\-.\\\/]|\[[0-9]{1,5}\])+$ + type: string + valuesKey: + description: |- + ValuesKey is the data key where the values.yaml or a specific value can be + found at. Defaults to 'values.yaml'. + maxLength: 253 + pattern: ^[\-._a-zA-Z0-9]+$ + type: string + required: + - kind + - name + type: object + type: array + required: + - interval + type: object + x-kubernetes-validations: + - message: either chart or chartRef must be set + rule: (has(self.chart) && !has(self.chartRef)) || (!has(self.chart) + && has(self.chartRef)) + status: + default: + observedGeneration: -1 + description: HelmReleaseStatus defines the observed state of a HelmRelease. + properties: + conditions: + description: Conditions holds the conditions for the HelmRelease. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + failures: + description: |- + Failures is the reconciliation failure count against the latest desired + state. It is reset after a successful reconciliation. + format: int64 + type: integer + helmChart: + description: |- + HelmChart is the namespaced name of the HelmChart resource created by + the controller for the HelmRelease. + type: string + history: + description: |- + History holds the history of Helm releases performed for this HelmRelease + up to the last successfully completed release. + items: + description: |- + Snapshot captures a point-in-time copy of the status information for a Helm release, + as managed by the controller. + properties: + apiVersion: + description: |- + APIVersion is the API version of the Snapshot. + Provisional: when the calculation method of the Digest field is changed, + this field will be used to distinguish between the old and new methods. + type: string + appVersion: + description: AppVersion is the chart app version of the release + object in storage. + type: string + chartName: + description: ChartName is the chart name of the release object + in storage. + type: string + chartVersion: + description: |- + ChartVersion is the chart version of the release object in + storage. + type: string + configDigest: + description: |- + ConfigDigest is the checksum of the config (better known as + "values") of the release object in storage. + It has the format of `:`. + type: string + deleted: + description: Deleted is when the release was deleted. + format: date-time + type: string + digest: + description: |- + Digest is the checksum of the release object in storage. + It has the format of `:`. + type: string + firstDeployed: + description: FirstDeployed is when the release was first deployed. + format: date-time + type: string + lastDeployed: + description: LastDeployed is when the release was last deployed. + format: date-time + type: string + name: + description: Name is the name of the release. + type: string + namespace: + description: Namespace is the namespace the release is deployed + to. + type: string + ociDigest: + description: OCIDigest is the digest of the OCI artifact associated + with the release. + type: string + status: + description: Status is the current state of the release. + type: string + testHooks: + additionalProperties: + description: |- + TestHookStatus holds the status information for a test hook as observed + to be run by the controller. + properties: + lastCompleted: + description: LastCompleted is the time the test hook last + completed. + format: date-time + type: string + lastStarted: + description: LastStarted is the time the test hook was + last started. + format: date-time + type: string + phase: + description: Phase the test hook was observed to be in. + type: string + type: object + description: |- + TestHooks is the list of test hooks for the release as observed to be + run by the controller. + type: object + version: + description: Version is the version of the release object in + storage. + type: integer + required: + - chartName + - chartVersion + - configDigest + - digest + - firstDeployed + - lastDeployed + - name + - namespace + - status + - version + type: object + type: array + installFailures: + description: |- + InstallFailures is the install failure count against the latest desired + state. It is reset after a successful reconciliation. + format: int64 + type: integer + lastAppliedRevision: + description: |- + LastAppliedRevision is the revision of the last successfully applied + source. + + Deprecated: the revision can now be found in the History. + type: string + lastAttemptedConfigDigest: + description: |- + LastAttemptedConfigDigest is the digest for the config (better known as + "values") of the last reconciliation attempt. + type: string + lastAttemptedGeneration: + description: |- + LastAttemptedGeneration is the last generation the controller attempted + to reconcile. + format: int64 + type: integer + lastAttemptedReleaseAction: + description: |- + LastAttemptedReleaseAction is the last release action performed for this + HelmRelease. It is used to determine the active remediation strategy. + enum: + - install + - upgrade + type: string + lastAttemptedRevision: + description: |- + LastAttemptedRevision is the Source revision of the last reconciliation + attempt. For OCIRepository sources, the 12 first characters of the digest are + appended to the chart version e.g. "1.2.3+1234567890ab". + type: string + lastAttemptedRevisionDigest: + description: |- + LastAttemptedRevisionDigest is the digest of the last reconciliation attempt. + This is only set for OCIRepository sources. + type: string + lastAttemptedValuesChecksum: + description: |- + LastAttemptedValuesChecksum is the SHA1 checksum for the values of the last + reconciliation attempt. + + Deprecated: Use LastAttemptedConfigDigest instead. + type: string + lastHandledForceAt: + description: |- + LastHandledForceAt holds the value of the most recent force request + value, so a change of the annotation value can be detected. + type: string + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + lastHandledResetAt: + description: |- + LastHandledResetAt holds the value of the most recent reset request + value, so a change of the annotation value can be detected. + type: string + lastReleaseRevision: + description: |- + LastReleaseRevision is the revision of the last successful Helm release. + + Deprecated: Use History instead. + type: integer + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + observedPostRenderersDigest: + description: |- + ObservedPostRenderersDigest is the digest for the post-renderers of + the last successful reconciliation attempt. + type: string + storageNamespace: + description: |- + StorageNamespace is the namespace of the Helm release storage for the + current release. + maxLength: 63 + minLength: 1 + type: string + upgradeFailures: + description: |- + UpgradeFailures is the upgrade failure count against the latest desired + state. It is reset after a successful reconciliation. + format: int64 + type: integer + type: object + type: object + served: true + storage: false + subresources: + status: {} diff --git a/crds/embedded/nelm-source-controller.yaml b/crds/embedded/nelm-source-controller.yaml new file mode 100644 index 0000000..482118d --- /dev/null +++ b/crds/embedded/nelm-source-controller.yaml @@ -0,0 +1,4102 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorbuckets.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorBucket + listKind: InternalNelmOperatorBucketList + plural: internalnelmoperatorbuckets + singular: internalnelmoperatorbucket + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.endpoint + name: Endpoint + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: Bucket is the Schema for the buckets API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + BucketSpec specifies the required configuration to produce an Artifact for + an object storage bucket. + properties: + bucketName: + description: BucketName is the name of the object storage bucket. + type: string + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + bucket. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + This field is only supported for the `generic` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + endpoint: + description: Endpoint is the object storage address the BucketName + is located at. + type: string + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + insecure: + description: Insecure allows connecting to a non-TLS HTTP Endpoint. + type: boolean + interval: + description: |- + Interval at which the Bucket Endpoint is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + prefix: + description: Prefix to use for server-side filtering of files in the + Bucket. + type: string + provider: + default: generic + description: |- + Provider of the object storage bucket. + Defaults to 'generic', which expects an S3 (API) compatible object + storage. + enum: + - generic + - aws + - gcp + - azure + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the Bucket server. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + region: + description: Region of the Endpoint where the BucketName is located + in. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the Bucket. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + the bucket. This field is only supported for the 'gcp' and 'aws' providers. + For more information about workload identity: + https://fluxcd.io/flux/components/source/buckets/#workload-identity + type: string + sts: + description: |- + STS specifies the required configuration to use a Security Token + Service for fetching temporary credentials to authenticate in a + Bucket provider. + + This field is only supported for the `aws` and `generic` providers. + properties: + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + STS endpoint. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + This field is only supported for the `ldap` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + endpoint: + description: |- + Endpoint is the HTTP/S endpoint of the Security Token Service from + where temporary credentials will be fetched. + pattern: ^(http|https)://.*$ + type: string + provider: + description: Provider of the Security Token Service. + enum: + - aws + - ldap + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the STS endpoint. This Secret must contain the fields `username` + and `password` and is supported only for the `ldap` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - endpoint + - provider + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + Bucket. + type: boolean + timeout: + default: 60s + description: Timeout for fetch operations, defaults to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + required: + - bucketName + - endpoint + - interval + type: object + x-kubernetes-validations: + - message: STS configuration is only supported for the 'aws' and 'generic' + Bucket providers + rule: self.provider == 'aws' || self.provider == 'generic' || !has(self.sts) + - message: '''aws'' is the only supported STS provider for the ''aws'' + Bucket provider' + rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider + == 'aws' + - message: '''ldap'' is the only supported STS provider for the ''generic'' + Bucket provider' + rule: self.provider != 'generic' || !has(self.sts) || self.sts.provider + == 'ldap' + - message: spec.sts.secretRef is not required for the 'aws' STS provider + rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)' + - message: spec.sts.certSecretRef is not required for the 'aws' STS provider + rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)' + - message: ServiceAccountName is not supported for the 'generic' Bucket + provider + rule: self.provider != 'generic' || !has(self.serviceAccountName) + - message: cannot set both .spec.secretRef and .spec.serviceAccountName + rule: '!has(self.secretRef) || !has(self.serviceAccountName)' + status: + default: + observedGeneration: -1 + description: BucketStatus records the observed state of a Bucket. + properties: + artifact: + description: Artifact represents the last successful Bucket reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the Bucket. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation of + the Bucket object. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + BucketStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.endpoint + name: Endpoint + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + deprecated: true + deprecationWarning: v1beta2 Bucket is deprecated, upgrade to v1 + name: v1beta2 + schema: + openAPIV3Schema: + description: Bucket is the Schema for the buckets API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + BucketSpec specifies the required configuration to produce an Artifact for + an object storage bucket. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + bucketName: + description: BucketName is the name of the object storage bucket. + type: string + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + bucket. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + This field is only supported for the `generic` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + endpoint: + description: Endpoint is the object storage address the BucketName + is located at. + type: string + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + insecure: + description: Insecure allows connecting to a non-TLS HTTP Endpoint. + type: boolean + interval: + description: |- + Interval at which the Bucket Endpoint is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + prefix: + description: Prefix to use for server-side filtering of files in the + Bucket. + type: string + provider: + default: generic + description: |- + Provider of the object storage bucket. + Defaults to 'generic', which expects an S3 (API) compatible object + storage. + enum: + - generic + - aws + - gcp + - azure + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the Bucket server. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + region: + description: Region of the Endpoint where the BucketName is located + in. + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the Bucket. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + sts: + description: |- + STS specifies the required configuration to use a Security Token + Service for fetching temporary credentials to authenticate in a + Bucket provider. + + This field is only supported for the `aws` and `generic` providers. + properties: + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + STS endpoint. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + This field is only supported for the `ldap` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + endpoint: + description: |- + Endpoint is the HTTP/S endpoint of the Security Token Service from + where temporary credentials will be fetched. + pattern: ^(http|https)://.*$ + type: string + provider: + description: Provider of the Security Token Service. + enum: + - aws + - ldap + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the STS endpoint. This Secret must contain the fields `username` + and `password` and is supported only for the `ldap` provider. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - endpoint + - provider + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + Bucket. + type: boolean + timeout: + default: 60s + description: Timeout for fetch operations, defaults to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + required: + - bucketName + - endpoint + - interval + type: object + x-kubernetes-validations: + - message: STS configuration is only supported for the 'aws' and 'generic' + Bucket providers + rule: self.provider == 'aws' || self.provider == 'generic' || !has(self.sts) + - message: '''aws'' is the only supported STS provider for the ''aws'' + Bucket provider' + rule: self.provider != 'aws' || !has(self.sts) || self.sts.provider + == 'aws' + - message: '''ldap'' is the only supported STS provider for the ''generic'' + Bucket provider' + rule: self.provider != 'generic' || !has(self.sts) || self.sts.provider + == 'ldap' + - message: spec.sts.secretRef is not required for the 'aws' STS provider + rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)' + - message: spec.sts.certSecretRef is not required for the 'aws' STS provider + rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)' + status: + default: + observedGeneration: -1 + description: BucketStatus records the observed state of a Bucket. + properties: + artifact: + description: Artifact represents the last successful Bucket reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the Bucket. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation of + the Bucket object. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + BucketStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorexternalartifacts.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorExternalArtifact + listKind: InternalNelmOperatorExternalArtifactList + plural: internalnelmoperatorexternalartifacts + singular: internalnelmoperatorexternalartifact + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .spec.sourceRef.name + name: Source + type: string + name: v1 + schema: + openAPIV3Schema: + description: ExternalArtifact is the Schema for the external artifacts API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: ExternalArtifactSpec defines the desired state of ExternalArtifact + properties: + sourceRef: + description: |- + SourceRef points to the Kubernetes custom resource for + which the artifact is generated. + properties: + apiVersion: + description: API version of the referent, if not specified the + Kubernetes preferred version will be used. + type: string + kind: + description: Kind of the referent. + type: string + name: + description: Name of the referent. + type: string + namespace: + description: Namespace of the referent, when not specified it + acts as LocalObjectReference. + type: string + required: + - kind + - name + type: object + type: object + status: + description: ExternalArtifactStatus defines the observed state of ExternalArtifact + properties: + artifact: + description: Artifact represents the output of an ExternalArtifact + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the ExternalArtifact. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorgitrepositories.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorGitRepository + listKind: InternalNelmOperatorGitRepositoryList + plural: internalnelmoperatorgitrepositories + shortNames: + - intnelmopgitrepo + singular: internalnelmoperatorgitrepository + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: GitRepository is the Schema for the gitrepositories API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + GitRepositorySpec specifies the required configuration to produce an + Artifact for a Git repository. + properties: + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + include: + description: |- + Include specifies a list of GitRepository resources which Artifacts + should be included in the Artifact produced for this GitRepository. + items: + description: |- + GitRepositoryInclude specifies a local reference to a GitRepository which + Artifact (sub-)contents must be included, and where they should be placed. + properties: + fromPath: + description: |- + FromPath specifies the path to copy contents from, defaults to the root + of the Artifact. + type: string + repository: + description: |- + GitRepositoryRef specifies the GitRepository which Artifact contents + must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: |- + ToPath specifies the path to copy contents to, defaults to the name of + the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + interval: + description: |- + Interval at which the GitRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + provider: + description: |- + Provider used for authentication, can be 'azure', 'github', 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - azure + - github + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the Git server. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + recurseSubmodules: + description: |- + RecurseSubmodules enables the initialization of all submodules within + the GitRepository as cloned from the URL, using their default settings. + type: boolean + ref: + description: |- + Reference specifies the Git reference to resolve and monitor for + changes, defaults to the 'master' branch. + properties: + branch: + description: Branch to check out, defaults to 'master' if no other + field is defined. + type: string + commit: + description: |- + Commit SHA to check out, takes precedence over all reference fields. + + This can be combined with Branch to shallow clone the branch, in which + the commit is expected to exist. + type: string + name: + description: |- + Name of the reference to check out; takes precedence over Branch, Tag and SemVer. + + It must be a valid Git reference: https://git-scm.com/docs/git-check-ref-format#_description + Examples: "refs/heads/main", "refs/tags/v0.1.0", "refs/pull/420/head", "refs/merge-requests/1/head" + type: string + semver: + description: SemVer tag expression to check out, takes precedence + over Tag. + type: string + tag: + description: Tag to check out, takes precedence over Branch. + type: string + type: object + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the GitRepository. + For HTTPS repositories the Secret must contain 'username' and 'password' + fields for basic auth or 'bearerToken' field for token auth. + For SSH repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to + authenticate to the GitRepository. This field is only supported for 'azure' provider. + type: string + sparseCheckout: + description: |- + SparseCheckout specifies a list of directories to checkout when cloning + the repository. If specified, only these directories are included in the + Artifact produced for this GitRepository. + items: + type: string + type: array + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + GitRepository. + type: boolean + timeout: + default: 60s + description: Timeout for Git operations like cloning, defaults to + 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: URL specifies the Git repository URL, it can be an HTTP/S + or SSH address. + pattern: ^(http|https|ssh)://.*$ + type: string + verify: + description: |- + Verification specifies the configuration to verify the Git commit + signature(s). + properties: + mode: + default: HEAD + description: |- + Mode specifies which Git object(s) should be verified. + + The variants "head" and "HEAD" both imply the same thing, i.e. verify + the commit that the HEAD of the Git repository points to. The variant + "head" solely exists to ensure backwards compatibility. + enum: + - head + - HEAD + - Tag + - TagAndHEAD + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing the public keys of trusted Git + authors. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - secretRef + type: object + required: + - interval + - url + type: object + x-kubernetes-validations: + - message: serviceAccountName can only be set when provider is 'azure' + rule: '!has(self.serviceAccountName) || (has(self.provider) && self.provider + == ''azure'')' + status: + default: + observedGeneration: -1 + description: GitRepositoryStatus records the observed state of a Git repository. + properties: + artifact: + description: Artifact represents the last successful GitRepository + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the GitRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + includedArtifacts: + description: |- + IncludedArtifacts contains a list of the last successfully included + Artifacts as instructed by GitRepositorySpec.Include. + items: + description: Artifact represents the output of a Source reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of + ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI + annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the GitRepository + object. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + observedInclude: + description: |- + ObservedInclude is the observed list of GitRepository resources used to + produce the current Artifact. + items: + description: |- + GitRepositoryInclude specifies a local reference to a GitRepository which + Artifact (sub-)contents must be included, and where they should be placed. + properties: + fromPath: + description: |- + FromPath specifies the path to copy contents from, defaults to the root + of the Artifact. + type: string + repository: + description: |- + GitRepositoryRef specifies the GitRepository which Artifact contents + must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: |- + ToPath specifies the path to copy contents to, defaults to the name of + the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + observedRecurseSubmodules: + description: |- + ObservedRecurseSubmodules is the observed resource submodules + configuration used to produce the current Artifact. + type: boolean + observedSparseCheckout: + description: |- + ObservedSparseCheckout is the observed list of directories used to + produce the current Artifact. + items: + type: string + type: array + sourceVerificationMode: + description: |- + SourceVerificationMode is the last used verification mode indicating + which Git object(s) have been verified. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + deprecated: true + deprecationWarning: v1beta2 GitRepository is deprecated, upgrade to v1 + name: v1beta2 + schema: + openAPIV3Schema: + description: GitRepository is the Schema for the gitrepositories API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + GitRepositorySpec specifies the required configuration to produce an + Artifact for a Git repository. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + gitImplementation: + default: go-git + description: |- + GitImplementation specifies which Git client library implementation to + use. Defaults to 'go-git', valid values are ('go-git', 'libgit2'). + Deprecated: gitImplementation is deprecated now that 'go-git' is the + only supported implementation. + enum: + - go-git + - libgit2 + type: string + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + include: + description: |- + Include specifies a list of GitRepository resources which Artifacts + should be included in the Artifact produced for this GitRepository. + items: + description: |- + GitRepositoryInclude specifies a local reference to a GitRepository which + Artifact (sub-)contents must be included, and where they should be placed. + properties: + fromPath: + description: |- + FromPath specifies the path to copy contents from, defaults to the root + of the Artifact. + type: string + repository: + description: |- + GitRepositoryRef specifies the GitRepository which Artifact contents + must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: |- + ToPath specifies the path to copy contents to, defaults to the name of + the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + interval: + description: Interval at which to check the GitRepository for updates. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + recurseSubmodules: + description: |- + RecurseSubmodules enables the initialization of all submodules within + the GitRepository as cloned from the URL, using their default settings. + type: boolean + ref: + description: |- + Reference specifies the Git reference to resolve and monitor for + changes, defaults to the 'master' branch. + properties: + branch: + description: Branch to check out, defaults to 'master' if no other + field is defined. + type: string + commit: + description: |- + Commit SHA to check out, takes precedence over all reference fields. + + This can be combined with Branch to shallow clone the branch, in which + the commit is expected to exist. + type: string + name: + description: |- + Name of the reference to check out; takes precedence over Branch, Tag and SemVer. + + It must be a valid Git reference: https://git-scm.com/docs/git-check-ref-format#_description + Examples: "refs/heads/main", "refs/tags/v0.1.0", "refs/pull/420/head", "refs/merge-requests/1/head" + type: string + semver: + description: SemVer tag expression to check out, takes precedence + over Tag. + type: string + tag: + description: Tag to check out, takes precedence over Branch. + type: string + type: object + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials for + the GitRepository. + For HTTPS repositories the Secret must contain 'username' and 'password' + fields for basic auth or 'bearerToken' field for token auth. + For SSH repositories the Secret must contain 'identity' + and 'known_hosts' fields. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + GitRepository. + type: boolean + timeout: + default: 60s + description: Timeout for Git operations like cloning, defaults to + 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: URL specifies the Git repository URL, it can be an HTTP/S + or SSH address. + pattern: ^(http|https|ssh)://.*$ + type: string + verify: + description: |- + Verification specifies the configuration to verify the Git commit + signature(s). + properties: + mode: + description: Mode specifies what Git object should be verified, + currently ('head'). + enum: + - head + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing the public keys of trusted Git + authors. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - mode + - secretRef + type: object + required: + - interval + - url + type: object + status: + default: + observedGeneration: -1 + description: GitRepositoryStatus records the observed state of a Git repository. + properties: + artifact: + description: Artifact represents the last successful GitRepository + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the GitRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + contentConfigChecksum: + description: |- + ContentConfigChecksum is a checksum of all the configurations related to + the content of the source artifact: + - .spec.ignore + - .spec.recurseSubmodules + - .spec.included and the checksum of the included artifacts + observed in .status.observedGeneration version of the object. This can + be used to determine if the content of the included repository has + changed. + It has the format of `:`, for example: `sha256:`. + + Deprecated: Replaced with explicit fields for observed artifact content + config in the status. + type: string + includedArtifacts: + description: |- + IncludedArtifacts contains a list of the last successfully included + Artifacts as instructed by GitRepositorySpec.Include. + items: + description: Artifact represents the output of a Source reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of + ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI + annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the GitRepository + object. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + observedInclude: + description: |- + ObservedInclude is the observed list of GitRepository resources used to + to produce the current Artifact. + items: + description: |- + GitRepositoryInclude specifies a local reference to a GitRepository which + Artifact (sub-)contents must be included, and where they should be placed. + properties: + fromPath: + description: |- + FromPath specifies the path to copy contents from, defaults to the root + of the Artifact. + type: string + repository: + description: |- + GitRepositoryRef specifies the GitRepository which Artifact contents + must be included. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + toPath: + description: |- + ToPath specifies the path to copy contents to, defaults to the name of + the GitRepositoryRef. + type: string + required: + - repository + type: object + type: array + observedRecurseSubmodules: + description: |- + ObservedRecurseSubmodules is the observed resource submodules + configuration used to produce the current Artifact. + type: boolean + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + GitRepositoryStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorhelmcharts.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorHelmChart + listKind: InternalNelmOperatorHelmChartList + plural: internalnelmoperatorhelmcharts + shortNames: + - intnelmophc + singular: internalnelmoperatorhelmchart + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.chart + name: Chart + type: string + - jsonPath: .spec.version + name: Version + type: string + - jsonPath: .spec.sourceRef.kind + name: Source Kind + type: string + - jsonPath: .spec.sourceRef.name + name: Source Name + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: HelmChart is the Schema for the helmcharts API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HelmChartSpec specifies the desired state of a Helm chart. + properties: + chart: + description: |- + Chart is the name or path the Helm chart is available at in the + SourceRef. + type: string + ignoreMissingValuesFiles: + description: |- + IgnoreMissingValuesFiles controls whether to silently ignore missing values + files rather than failing. + type: boolean + interval: + description: |- + Interval at which the HelmChart SourceRef is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + reconcileStrategy: + default: ChartVersion + description: |- + ReconcileStrategy determines what enables the creation of a new artifact. + Valid values are ('ChartVersion', 'Revision'). + See the documentation of the values for an explanation on their behavior. + Defaults to ChartVersion when omitted. + enum: + - ChartVersion + - Revision + type: string + sourceRef: + description: SourceRef is the reference to the Source the chart is + available at. + properties: + apiVersion: + description: APIVersion of the referent. + type: string + kind: + description: |- + Kind of the referent, valid values are ('HelmRepository', 'GitRepository', + 'Bucket'). + enum: + - InternalNelmOperatorHelmRepository + - InternalNelmOperatorGitRepository + - InternalNelmOperatorBucket + type: string + name: + description: Name of the referent. + type: string + required: + - kind + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + source. + type: boolean + valuesFiles: + description: |- + ValuesFiles is an alternative list of values files to use as the chart + values (values.yaml is not included by default), expected to be a + relative path in the SourceRef. + Values files are merged in the order of this list with the last file + overriding the first. Ignored when omitted. + items: + type: string + type: array + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + This field is only supported when using HelmRepository source with spec.type 'oci'. + Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified. + properties: + matchOIDCIdentity: + description: |- + MatchOIDCIdentity specifies the identity matching criteria to use + while verifying an OCI artifact which was signed using Cosign keyless + signing. The artifact's identity is deemed to be verified if any of the + specified matchers match against the identity. + items: + description: |- + OIDCIdentityMatch specifies options for verifying the certificate identity, + i.e. the issuer and the subject of the certificate. + properties: + issuer: + description: |- + Issuer specifies the regex pattern to match against to verify + the OIDC issuer in the Fulcio certificate. The pattern must be a + valid Go regular expression. + type: string + subject: + description: |- + Subject specifies the regex pattern to match against to verify + the identity subject in the Fulcio certificate. The pattern must + be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array + provider: + default: cosign + description: Provider specifies the technology used to sign the + OCI Artifact. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + version: + default: '*' + description: |- + Version is the chart version semver expression, ignored for charts from + GitRepository and Bucket sources. Defaults to latest when omitted. + type: string + required: + - chart + - interval + - sourceRef + type: object + x-kubernetes-validations: + - message: spec.verify is only supported when spec.sourceRef.kind is 'HelmRepository' + rule: '!has(self.verify) || self.sourceRef.kind == ''HelmRepository''' + status: + default: + observedGeneration: -1 + description: HelmChartStatus records the observed state of the HelmChart. + properties: + artifact: + description: Artifact represents the output of the last successful + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the HelmChart. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedChartName: + description: |- + ObservedChartName is the last observed chart name as specified by the + resolved chart reference. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the HelmChart + object. + format: int64 + type: integer + observedSourceArtifactRevision: + description: |- + ObservedSourceArtifactRevision is the last observed Artifact.Revision + of the HelmChartSpec.SourceRef. + type: string + observedValuesFiles: + description: |- + ObservedValuesFiles are the observed value files of the last successful + reconciliation. + It matches the chart in the last successfully reconciled artifact. + items: + type: string + type: array + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + BucketStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.chart + name: Chart + type: string + - jsonPath: .spec.version + name: Version + type: string + - jsonPath: .spec.sourceRef.kind + name: Source Kind + type: string + - jsonPath: .spec.sourceRef.name + name: Source Name + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + deprecated: true + deprecationWarning: v1beta2 HelmChart is deprecated, upgrade to v1 + name: v1beta2 + schema: + openAPIV3Schema: + description: HelmChart is the Schema for the helmcharts API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: HelmChartSpec specifies the desired state of a Helm chart. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + chart: + description: |- + Chart is the name or path the Helm chart is available at in the + SourceRef. + type: string + ignoreMissingValuesFiles: + description: |- + IgnoreMissingValuesFiles controls whether to silently ignore missing values + files rather than failing. + type: boolean + interval: + description: |- + Interval at which the HelmChart SourceRef is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + reconcileStrategy: + default: ChartVersion + description: |- + ReconcileStrategy determines what enables the creation of a new artifact. + Valid values are ('ChartVersion', 'Revision'). + See the documentation of the values for an explanation on their behavior. + Defaults to ChartVersion when omitted. + enum: + - ChartVersion + - Revision + type: string + sourceRef: + description: SourceRef is the reference to the Source the chart is + available at. + properties: + apiVersion: + description: APIVersion of the referent. + type: string + kind: + description: |- + Kind of the referent, valid values are ('HelmRepository', 'GitRepository', + 'Bucket'). + enum: + - InternalNelmOperatorHelmRepository + - InternalNelmOperatorGitRepository + - InternalNelmOperatorBucket + type: string + name: + description: Name of the referent. + type: string + required: + - kind + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + source. + type: boolean + valuesFile: + description: |- + ValuesFile is an alternative values file to use as the default chart + values, expected to be a relative path in the SourceRef. Deprecated in + favor of ValuesFiles, for backwards compatibility the file specified here + is merged before the ValuesFiles items. Ignored when omitted. + type: string + valuesFiles: + description: |- + ValuesFiles is an alternative list of values files to use as the chart + values (values.yaml is not included by default), expected to be a + relative path in the SourceRef. + Values files are merged in the order of this list with the last file + overriding the first. Ignored when omitted. + items: + type: string + type: array + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + This field is only supported when using HelmRepository source with spec.type 'oci'. + Chart dependencies, which are not bundled in the umbrella chart artifact, are not verified. + properties: + matchOIDCIdentity: + description: |- + MatchOIDCIdentity specifies the identity matching criteria to use + while verifying an OCI artifact which was signed using Cosign keyless + signing. The artifact's identity is deemed to be verified if any of the + specified matchers match against the identity. + items: + description: |- + OIDCIdentityMatch specifies options for verifying the certificate identity, + i.e. the issuer and the subject of the certificate. + properties: + issuer: + description: |- + Issuer specifies the regex pattern to match against to verify + the OIDC issuer in the Fulcio certificate. The pattern must be a + valid Go regular expression. + type: string + subject: + description: |- + Subject specifies the regex pattern to match against to verify + the identity subject in the Fulcio certificate. The pattern must + be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array + provider: + default: cosign + description: Provider specifies the technology used to sign the + OCI Artifact. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + version: + default: '*' + description: |- + Version is the chart version semver expression, ignored for charts from + GitRepository and Bucket sources. Defaults to latest when omitted. + type: string + required: + - chart + - interval + - sourceRef + type: object + status: + default: + observedGeneration: -1 + description: HelmChartStatus records the observed state of the HelmChart. + properties: + artifact: + description: Artifact represents the output of the last successful + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the HelmChart. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedChartName: + description: |- + ObservedChartName is the last observed chart name as specified by the + resolved chart reference. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the HelmChart + object. + format: int64 + type: integer + observedSourceArtifactRevision: + description: |- + ObservedSourceArtifactRevision is the last observed Artifact.Revision + of the HelmChartSpec.SourceRef. + type: string + observedValuesFiles: + description: |- + ObservedValuesFiles are the observed value files of the last successful + reconciliation. + It matches the chart in the last successfully reconciled artifact. + items: + type: string + type: array + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + BucketStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorhelmrepositories.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorHelmRepository + listKind: InternalNelmOperatorHelmRepositoryList + plural: internalnelmoperatorhelmrepositories + shortNames: + - intnelmophelmrepo + singular: internalnelmoperatorhelmrepository + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + name: v1 + schema: + openAPIV3Schema: + description: HelmRepository is the Schema for the helmrepositories API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + HelmRepositorySpec specifies the required configuration to produce an + Artifact for a Helm repository index YAML. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + It takes precedence over the values specified in the Secret referred + to by `.spec.secretRef`. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + insecure: + description: |- + Insecure allows connecting to a non-TLS HTTP container registry. + This field is only taken into account if the .spec.type field is set to 'oci'. + type: boolean + interval: + description: |- + Interval at which the HelmRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + passCredentials: + description: |- + PassCredentials allows the credentials from the SecretRef to be passed + on to a host that does not match the host as defined in URL. + This may be required if the host of the advertised chart URLs in the + index differ from the defined URL. + Enabling this should be done with caution, as it can potentially result + in credentials getting stolen in a MITM-attack. + type: boolean + provider: + default: generic + description: |- + Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + This field is optional, and only taken into account if the .spec.type field is set to 'oci'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the HelmRepository. + For HTTP/S basic auth the secret must contain 'username' and 'password' + fields. + Support for TLS auth using the 'certFile' and 'keyFile', and/or 'caFile' + keys is deprecated. Please use `.spec.certSecretRef` instead. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + HelmRepository. + type: boolean + timeout: + description: |- + Timeout is used for the index fetch operation for an HTTPS helm repository, + and for remote OCI Repository operations like pulling for an OCI helm + chart by the associated HelmChart. + Its default value is 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + type: + description: |- + Type of the HelmRepository. + When this field is set to "oci", the URL field value must be prefixed with "oci://". + enum: + - default + - oci + type: string + url: + description: |- + URL of the Helm repository, a valid URL contains at least a protocol and + host. + pattern: ^(http|https|oci)://.*$ + type: string + required: + - url + type: object + status: + default: + observedGeneration: -1 + description: HelmRepositoryStatus records the observed state of the HelmRepository. + properties: + artifact: + description: Artifact represents the last successful HelmRepository + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the HelmRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the HelmRepository + object. + format: int64 + type: integer + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + HelmRepositoryStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + deprecated: true + deprecationWarning: v1beta2 HelmRepository is deprecated, upgrade to v1 + name: v1beta2 + schema: + openAPIV3Schema: + description: HelmRepository is the Schema for the helmrepositories API. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: |- + HelmRepositorySpec specifies the required configuration to produce an + Artifact for a Helm repository index YAML. + properties: + accessFrom: + description: |- + AccessFrom specifies an Access Control List for allowing cross-namespace + references to this object. + NOTE: Not implemented, provisional as of https://github.com/fluxcd/flux2/pull/2092 + properties: + namespaceSelectors: + description: |- + NamespaceSelectors is the list of namespace selectors to which this ACL applies. + Items in this list are evaluated using a logical OR operation. + items: + description: |- + NamespaceSelector selects the namespaces to which this ACL applies. + An empty map of MatchLabels matches all namespaces in a cluster. + properties: + matchLabels: + additionalProperties: + type: string + description: |- + MatchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, whose key field is "key", the + operator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + type: array + required: + - namespaceSelectors + type: object + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + It takes precedence over the values specified in the Secret referred + to by `.spec.secretRef`. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + insecure: + description: |- + Insecure allows connecting to a non-TLS HTTP container registry. + This field is only taken into account if the .spec.type field is set to 'oci'. + type: boolean + interval: + description: |- + Interval at which the HelmRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + passCredentials: + description: |- + PassCredentials allows the credentials from the SecretRef to be passed + on to a host that does not match the host as defined in URL. + This may be required if the host of the advertised chart URLs in the + index differ from the defined URL. + Enabling this should be done with caution, as it can potentially result + in credentials getting stolen in a MITM-attack. + type: boolean + provider: + default: generic + description: |- + Provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + This field is optional, and only taken into account if the .spec.type field is set to 'oci'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + secretRef: + description: |- + SecretRef specifies the Secret containing authentication credentials + for the HelmRepository. + For HTTP/S basic auth the secret must contain 'username' and 'password' + fields. + Support for TLS auth using the 'certFile' and 'keyFile', and/or 'caFile' + keys is deprecated. Please use `.spec.certSecretRef` instead. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + suspend: + description: |- + Suspend tells the controller to suspend the reconciliation of this + HelmRepository. + type: boolean + timeout: + description: |- + Timeout is used for the index fetch operation for an HTTPS helm repository, + and for remote OCI Repository operations like pulling for an OCI helm + chart by the associated HelmChart. + Its default value is 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + type: + description: |- + Type of the HelmRepository. + When this field is set to "oci", the URL field value must be prefixed with "oci://". + enum: + - default + - oci + type: string + url: + description: |- + URL of the Helm repository, a valid URL contains at least a protocol and + host. + pattern: ^(http|https|oci)://.*$ + type: string + required: + - url + type: object + status: + default: + observedGeneration: -1 + description: HelmRepositoryStatus records the observed state of the HelmRepository. + properties: + artifact: + description: Artifact represents the last successful HelmRepository + reconciliation. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the HelmRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: |- + ObservedGeneration is the last observed generation of the HelmRepository + object. + format: int64 + type: integer + url: + description: |- + URL is the dynamic fetch link for the latest Artifact. + It is provided on a "best effort" basis, and using the precise + HelmRepositoryStatus.Artifact data is recommended. + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.19.0 + labels: + backup.deckhouse.io/cluster-config: "true" + heritage: deckhouse + module: operator-helm + name: internalnelmoperatorocirepositories.source.internal.operator-helm.deckhouse.io +spec: + group: source.internal.operator-helm.deckhouse.io + names: + kind: InternalNelmOperatorOCIRepository + listKind: InternalNelmOperatorOCIRepositoryList + plural: internalnelmoperatorocirepositories + shortNames: + - intnelmopocirepo + singular: internalnelmoperatorocirepository + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: OCIRepository is the Schema for the ocirepositories API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OCIRepositorySpec defines the desired state of OCIRepository + properties: + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + insecure: + description: Insecure allows connecting to a non-TLS HTTP container + registry. + type: boolean + interval: + description: |- + Interval at which the OCIRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + layerSelector: + description: |- + LayerSelector specifies which layer should be extracted from the OCI artifact. + When not specified, the first layer found in the artifact is selected. + properties: + mediaType: + description: |- + MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The + first layer matching this type is selected. + type: string + operation: + description: |- + Operation specifies how the selected layer should be processed. + By default, the layer compressed content is extracted to storage. + When the operation is set to 'copy', the layer compressed content + is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object + provider: + default: generic + description: |- + The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the container registry. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ref: + description: |- + The OCI reference to pull and monitor for changes, + defaults to the latest tag. + properties: + digest: + description: |- + Digest is the image digest to pull, takes precedence over SemVer. + The value should be in the format 'sha256:'. + type: string + semver: + description: |- + SemVer is the range of tags to pull selecting the latest within + the range, takes precedence over Tag. + type: string + semverFilter: + description: SemverFilter is a regex pattern to filter the tags + within the SemVer range. + type: string + tag: + description: Tag is the image tag to pull, defaults to latest. + type: string + type: object + secretRef: + description: |- + SecretRef contains the secret name containing the registry login + credentials to resolve image metadata. + The secret must be of type kubernetes.io/dockerconfigjson. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + the image pull if the service account has attached pull secrets. For more information: + https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account + type: string + suspend: + description: This flag tells the controller to suspend the reconciliation + of this source. + type: boolean + timeout: + default: 60s + description: The timeout for remote OCI Repository operations like + pulling, defaults to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: |- + URL is a reference to an OCI artifact repository hosted + on a remote container registry. + pattern: ^oci://.*$ + type: string + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + properties: + matchOIDCIdentity: + description: |- + MatchOIDCIdentity specifies the identity matching criteria to use + while verifying an OCI artifact which was signed using Cosign keyless + signing. The artifact's identity is deemed to be verified if any of the + specified matchers match against the identity. + items: + description: |- + OIDCIdentityMatch specifies options for verifying the certificate identity, + i.e. the issuer and the subject of the certificate. + properties: + issuer: + description: |- + Issuer specifies the regex pattern to match against to verify + the OIDC issuer in the Fulcio certificate. The pattern must be a + valid Go regular expression. + type: string + subject: + description: |- + Subject specifies the regex pattern to match against to verify + the identity subject in the Fulcio certificate. The pattern must + be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array + provider: + default: cosign + description: Provider specifies the technology used to sign the + OCI Artifact. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + required: + - interval + - url + type: object + status: + default: + observedGeneration: -1 + description: OCIRepositoryStatus defines the observed state of OCIRepository + properties: + artifact: + description: Artifact represents the output of the last successful + OCI Repository sync. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the OCIRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + observedLayerSelector: + description: |- + ObservedLayerSelector is the observed layer selector used for constructing + the source artifact. + properties: + mediaType: + description: |- + MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The + first layer matching this type is selected. + type: string + operation: + description: |- + Operation specifies how the selected layer should be processed. + By default, the layer compressed content is extracted to storage. + When the operation is set to 'copy', the layer compressed content + is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object + url: + description: URL is the download link for the artifact output of the + last OCI Repository sync. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} + - additionalPrinterColumns: + - jsonPath: .spec.url + name: URL + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + deprecated: true + deprecationWarning: v1beta2 OCIRepository is deprecated, upgrade to v1 + name: v1beta2 + schema: + openAPIV3Schema: + description: OCIRepository is the Schema for the ocirepositories API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: OCIRepositorySpec defines the desired state of OCIRepository + properties: + certSecretRef: + description: |- + CertSecretRef can be given the name of a Secret containing + either or both of + + - a PEM-encoded client certificate (`tls.crt`) and private + key (`tls.key`); + - a PEM-encoded CA certificate (`ca.crt`) + + and whichever are supplied, will be used for connecting to the + registry. The client cert and key are useful if you are + authenticating with a certificate; the CA cert is useful if + you are using a self-signed server certificate. The Secret must + be of type `Opaque` or `kubernetes.io/tls`. + + Note: Support for the `caFile`, `certFile` and `keyFile` keys have + been deprecated. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ignore: + description: |- + Ignore overrides the set of excluded patterns in the .sourceignore format + (which is the same as .gitignore). If not provided, a default will be used, + consult the documentation for your version to find out what those are. + type: string + insecure: + description: Insecure allows connecting to a non-TLS HTTP container + registry. + type: boolean + interval: + description: |- + Interval at which the OCIRepository URL is checked for updates. + This interval is approximate and may be subject to jitter to ensure + efficient use of resources. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m|h))+$ + type: string + layerSelector: + description: |- + LayerSelector specifies which layer should be extracted from the OCI artifact. + When not specified, the first layer found in the artifact is selected. + properties: + mediaType: + description: |- + MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The + first layer matching this type is selected. + type: string + operation: + description: |- + Operation specifies how the selected layer should be processed. + By default, the layer compressed content is extracted to storage. + When the operation is set to 'copy', the layer compressed content + is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object + provider: + default: generic + description: |- + The provider used for authentication, can be 'aws', 'azure', 'gcp' or 'generic'. + When not specified, defaults to 'generic'. + enum: + - generic + - aws + - azure + - gcp + type: string + proxySecretRef: + description: |- + ProxySecretRef specifies the Secret containing the proxy configuration + to use while communicating with the container registry. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + ref: + description: |- + The OCI reference to pull and monitor for changes, + defaults to the latest tag. + properties: + digest: + description: |- + Digest is the image digest to pull, takes precedence over SemVer. + The value should be in the format 'sha256:'. + type: string + semver: + description: |- + SemVer is the range of tags to pull selecting the latest within + the range, takes precedence over Tag. + type: string + semverFilter: + description: SemverFilter is a regex pattern to filter the tags + within the SemVer range. + type: string + tag: + description: Tag is the image tag to pull, defaults to latest. + type: string + type: object + secretRef: + description: |- + SecretRef contains the secret name containing the registry login + credentials to resolve image metadata. + The secret must be of type kubernetes.io/dockerconfigjson. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + serviceAccountName: + description: |- + ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate + the image pull if the service account has attached pull secrets. For more information: + https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#add-imagepullsecrets-to-a-service-account + type: string + suspend: + description: This flag tells the controller to suspend the reconciliation + of this source. + type: boolean + timeout: + default: 60s + description: The timeout for remote OCI Repository operations like + pulling, defaults to 60s. + pattern: ^([0-9]+(\.[0-9]+)?(ms|s|m))+$ + type: string + url: + description: |- + URL is a reference to an OCI artifact repository hosted + on a remote container registry. + pattern: ^oci://.*$ + type: string + verify: + description: |- + Verify contains the secret name containing the trusted public keys + used to verify the signature and specifies which provider to use to check + whether OCI image is authentic. + properties: + matchOIDCIdentity: + description: |- + MatchOIDCIdentity specifies the identity matching criteria to use + while verifying an OCI artifact which was signed using Cosign keyless + signing. The artifact's identity is deemed to be verified if any of the + specified matchers match against the identity. + items: + description: |- + OIDCIdentityMatch specifies options for verifying the certificate identity, + i.e. the issuer and the subject of the certificate. + properties: + issuer: + description: |- + Issuer specifies the regex pattern to match against to verify + the OIDC issuer in the Fulcio certificate. The pattern must be a + valid Go regular expression. + type: string + subject: + description: |- + Subject specifies the regex pattern to match against to verify + the identity subject in the Fulcio certificate. The pattern must + be a valid Go regular expression. + type: string + required: + - issuer + - subject + type: object + type: array + provider: + default: cosign + description: Provider specifies the technology used to sign the + OCI Artifact. + enum: + - cosign + - notation + type: string + secretRef: + description: |- + SecretRef specifies the Kubernetes Secret containing the + trusted public keys. + properties: + name: + description: Name of the referent. + type: string + required: + - name + type: object + required: + - provider + type: object + required: + - interval + - url + type: object + status: + default: + observedGeneration: -1 + description: OCIRepositoryStatus defines the observed state of OCIRepository + properties: + artifact: + description: Artifact represents the output of the last successful + OCI Repository sync. + properties: + digest: + description: Digest is the digest of the file in the form of ':'. + pattern: ^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-zA-Z0-9=_-]+$ + type: string + lastUpdateTime: + description: |- + LastUpdateTime is the timestamp corresponding to the last update of the + Artifact. + format: date-time + type: string + metadata: + additionalProperties: + type: string + description: Metadata holds upstream information such as OCI annotations. + type: object + path: + description: |- + Path is the relative file path of the Artifact. It can be used to locate + the file in the root of the Artifact storage on the local file system of + the controller managing the Source. + type: string + revision: + description: |- + Revision is a human-readable identifier traceable in the origin source + system. It can be a Git commit SHA, Git tag, a Helm chart version, etc. + type: string + size: + description: Size is the number of bytes in the file. + format: int64 + type: integer + url: + description: |- + URL is the HTTP address of the Artifact as exposed by the controller + managing the Source. It can be used to retrieve the Artifact for + consumption, e.g. by another controller applying the Artifact contents. + type: string + required: + - digest + - lastUpdateTime + - path + - revision + - url + type: object + conditions: + description: Conditions holds the conditions for the OCIRepository. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + contentConfigChecksum: + description: |- + ContentConfigChecksum is a checksum of all the configurations related to + the content of the source artifact: + - .spec.ignore + - .spec.layerSelector + observed in .status.observedGeneration version of the object. This can + be used to determine if the content configuration has changed and the + artifact needs to be rebuilt. + It has the format of `:`, for example: `sha256:`. + + Deprecated: Replaced with explicit fields for observed artifact content + config in the status. + type: string + lastHandledReconcileAt: + description: |- + LastHandledReconcileAt holds the value of the most recent + reconcile request value, so a change of the annotation value + can be detected. + type: string + observedGeneration: + description: ObservedGeneration is the last observed generation. + format: int64 + type: integer + observedIgnore: + description: |- + ObservedIgnore is the observed exclusion patterns used for constructing + the source artifact. + type: string + observedLayerSelector: + description: |- + ObservedLayerSelector is the observed layer selector used for constructing + the source artifact. + properties: + mediaType: + description: |- + MediaType specifies the OCI media type of the layer + which should be extracted from the OCI Artifact. The + first layer matching this type is selected. + type: string + operation: + description: |- + Operation specifies how the selected layer should be processed. + By default, the layer compressed content is extracted to storage. + When the operation is set to 'copy', the layer compressed content + is persisted to storage as it is. + enum: + - extract + - copy + type: string + type: object + url: + description: URL is the download link for the artifact output of the + last OCI Repository sync. + type: string + type: object + type: object + served: true + storage: false + subresources: + status: {} diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md new file mode 100644 index 0000000..2e159c7 --- /dev/null +++ b/docs/CONFIGURATION.md @@ -0,0 +1,4 @@ +--- +title: "Module configuration" +weight: 30 +--- diff --git a/docs/CONFIGURATION_RU.md b/docs/CONFIGURATION_RU.md new file mode 100644 index 0000000..2e159c7 --- /dev/null +++ b/docs/CONFIGURATION_RU.md @@ -0,0 +1,4 @@ +--- +title: "Module configuration" +weight: 30 +--- diff --git a/docs/CR.md b/docs/CR.md new file mode 100644 index 0000000..4f1f169 --- /dev/null +++ b/docs/CR.md @@ -0,0 +1,4 @@ +--- +title: "Custom Resources" +weight: 60 +--- diff --git a/docs/CR_RU.md b/docs/CR_RU.md new file mode 100644 index 0000000..4f1f169 --- /dev/null +++ b/docs/CR_RU.md @@ -0,0 +1,4 @@ +--- +title: "Custom Resources" +weight: 60 +--- diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..aa916b9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,16 @@ +--- +title: "operator-helm" +menuTitle: "operator-helm" +moduleStatus: General Availability +weight: 10 +--- + +The `operator-helm` module allows you to declaratively manage Helm applications and associated resources. + +## Usage scenarios + + + +## Architecture + + diff --git a/docs/README_RU.md b/docs/README_RU.md new file mode 100644 index 0000000..e88efe5 --- /dev/null +++ b/docs/README_RU.md @@ -0,0 +1,16 @@ +--- +title: "operator-helm" +menuTitle: "operator-helm" +moduleStatus: General Availability +weight: 10 +--- + +Модуль `operator-helm` позволяет декларативно управлять Helm приложениями и связанными с ними ресурсами. + +## Сценарии использования + + + +## Архитектура + + diff --git a/docs/images/.keep b/docs/images/.keep new file mode 100644 index 0000000..e69de29 diff --git a/docs/internal/components_placement.md b/docs/internal/components_placement.md new file mode 100644 index 0000000..17bc1d2 --- /dev/null +++ b/docs/internal/components_placement.md @@ -0,0 +1,3 @@ +## Placement strategies + + diff --git a/hooks/.keep b/hooks/.keep new file mode 100644 index 0000000..e69de29 diff --git a/images/helm-controller/werf.inc.yaml b/images/helm-controller/werf.inc.yaml new file mode 100644 index 0000000..265b860 --- /dev/null +++ b/images/helm-controller/werf.inc.yaml @@ -0,0 +1,3 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +from: registry.werf.io/nelm/helm-controller:v0.1.3 diff --git a/images/kube-api-rewriter/.dockerignore b/images/kube-api-rewriter/.dockerignore new file mode 100644 index 0000000..e5a9ac0 --- /dev/null +++ b/images/kube-api-rewriter/.dockerignore @@ -0,0 +1,9 @@ +.git +*.log +*.swp + +templates +Chart.yaml + +golangci-lint +proxy diff --git a/images/kube-api-rewriter/.gitignore b/images/kube-api-rewriter/.gitignore new file mode 100644 index 0000000..eeb1ad6 --- /dev/null +++ b/images/kube-api-rewriter/.gitignore @@ -0,0 +1 @@ +!pkg/log diff --git a/images/kube-api-rewriter/METRICS.md b/images/kube-api-rewriter/METRICS.md new file mode 100644 index 0000000..f7e3679 --- /dev/null +++ b/images/kube-api-rewriter/METRICS.md @@ -0,0 +1,166 @@ +# Metrics + +## Custom metrics + +These metrics describe proxy instances performance. + +### kube_api_rewriter_client_requests_total + +Total number of received client requests. + +Type: counter + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: pass request Body as-is or rewrite its content. + +### kube_api_rewriter_target_responses_total + +Total number of responses from the target. + +Type: counter + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: pass request Body as-is or rewrite its content. +- status - HTTP status of the target response. +- error - 0 if no error, 1 if error occurred. + +### kube_api_rewriter_target_response_invalid_json_total + +Total target responses with invalid JSON. Can be used to catch accidental Protobuf responses. + +Type: counter + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- status - HTTP status of the target response. + +### kube_api_rewriter_requests_handled_total + +Total number of requests handled by the proxy instance. + +Type: counter + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` target response Body as-is or `rewrite` its content. +- status - HTTP status of the target response. +- error - 0 if no error, 1 if error occurred. + + +### kube_api_rewriter_request_handling_duration_seconds + +Duration of request handling for non-watching and watch event handling for watch requests + +Type: histogram + +Buckets: 1, 2, 5 ms, 10, 20, 50 ms, 100, 200, 500 ms, 1, 2, 5 s + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` target response Body as-is or `rewrite` its content. +- status - HTTP status of the target response. + +### kube_api_rewriter_rewrites_total + +Total rewrites executed by the proxy instance. + +Type: counter + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- side - What was rewritten: `client` request or `target` response. +- operation - Rewrite operation: restore or rename. +- error - 0 if no error, 1 if error occurred. + +### kube_api_rewriter_rewrite_duration_seconds + +Duration of rewrite operations. + +Type: histogram + +Buckets: 1, 2, 5 ms, 10, 20, 50 ms, 100, 200, 500 ms, 1, 2, 5 s + +Labels: +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- side - What was rewritten: `client` request or `target` response. +- operation - Rewrite operation: restore or rename. + +### kube_api_rewriter_from_client_bytes_total + +Total bytes received from the client. + +Type: counter + +Labels: + +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` client request Body as-is or `rewrite` its content. + +### kube_api_rewriter_to_target_bytes_total + +Total bytes transferred to the target. + +Type: counter + +Labels: + +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` client request Body as-is or `rewrite` its content. + +### kube_api_rewriter_from_target_bytes_total + +Total bytes received from the target. + +Type: counter + +Labels: + +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` target response Body as-is or `rewrite` its content. + +### kube_api_rewriter_to_client_bytes_total + +Total bytes transferred back to the client. + +Type: counter + +Labels: + +- name - Proxy instance name (kube-api or webhook). +- resource - Kubernetes resource type from url path. +- method - HTTP method of the request. +- watch - Is watch stream requested? (watch=true in the url query). +- decision - proxy decision: `pass` target response Body as-is or `rewrite` its content. + diff --git a/images/kube-api-rewriter/STRUCTURE.md b/images/kube-api-rewriter/STRUCTURE.md new file mode 100644 index 0000000..bdbf4a2 --- /dev/null +++ b/images/kube-api-rewriter/STRUCTURE.md @@ -0,0 +1,451 @@ +# kube-api-rewriter structure + +The idea of the rewriter proxy is simple: make controller connect to the local +proxy in the sidecar, so proxy will pass requests to real Kubernetes API Server. +Proxy may rewrite JSON payloads for different purposes, e.g. resources renaming. + +Kube-api-rewriter contains 2 proxy instances: +- "api" proxy to handle usual API requests from the proxied controller to the Kubernetes API Server. +- "webhook" proxy to handle webhook requests from the Kubernetes API Server to the proxied controller. + + +Example setup: rename resources for Kubevirt. +```mermaid +%%{init: {"flowchart": {"htmlLabels": false}} }%% +flowchart TB + NoProxy-.->WithProxy + + subgraph NoProxy ["`**Original Kubevirt setup**`"] + direction TB + + subgraph np-virt-operator-deploy ["`Deploy/virt-operator`"] + np-virt-operator("`container + name: virt-operator`") + end + + subgraph np-virt-controller-deploy ["`Deploy/virt-controller`"] + np-virt-controller("`container + name: virt-controller`") + end + + np-kube-api["`Kubernetes API Server + with resources in apiGroup + *.kubevirt.io*`"] + + np-virt-operator <-- "Original resources + in API calls" --> np-kube-api + np-virt-controller <-- "Original resources + in API calls" --> np-kube-api + end + subgraph WithProxy ["`**Kubevirt with proxy**`"] + direction TB + + subgraph p-virt-operator-deploy ["`Deploy/virt-operator`"] + p-virt-operator("`container + name: virt-operator`") + p-virt-operator-proxy{{"container + name: proxy"}} + p-virt-operator -- "Original resources + in API calls" --> p-virt-operator-proxy + p-virt-operator-proxy -- "Restored resources + in API responses" --> p-virt-operator + end + + subgraph p-virt-controller-deploy ["`Deploy/virt-controller`"] + p-virt-controller("`container + name: virt-controller`") + p-virt-controller-proxy{{"container + name: proxy"}} + p-virt-controller -- "Original resources +in API calls" --> p-virt-controller-proxy + p-virt-controller-proxy -- "Restored resources + in API responses" --> p-virt-controller + end + + p-kube-api["`Kubernetes API Server + with resources in apiGroup + *.x.virtualization.deckhouse.io*`"] + + p-virt-operator-proxy <-- "Renamed resources in + API calls" --> p-kube-api + p-virt-controller-proxy <-- "Renamed resources in + API calls" --> p-kube-api + end +``` + +All DVP components: +```mermaid +%%{init: {"flowchart": {"htmlLabels": false}} }%% +flowchart + subgraph kubevirt ["Kubevirt"] + subgraph virt-operator-deploy ["`Deploy/virt-operator`"] + virt-operator("`container: + virt-operator`") + virt-operator-proxy{{"container: + proxy"}} + virt-operator --> virt-operator-proxy + virt-operator-proxy --> virt-operator + end + + subgraph p-virt-controller-deploy ["`Deploy/virt-controller`"] + virt-controller("`container: + virt-controller`") + virt-controller-proxy{{"container: + proxy"}} + virt-controller --> virt-controller-proxy + virt-controller-proxy --> virt-controller + end + subgraph p-virt-api-deploy ["`Deploy/virt-api`"] + virt-api("`container: + virt-api`") + virt-api-proxy{{"container: + proxy"}} + virt-api --> virt-api-proxy + virt-api-proxy --> virt-api + end + + subgraph p-virt-handler-deploy ["`DaemonSet/virt-handler`"] + virt-handler("`container: + virt-handler`") + virt-handler-proxy{{"container: + proxy"}} + virt-handler --> virt-handler-proxy + virt-handler-proxy --> virt-handler + end + end + + subgraph kubeapi ["control-plane"] + kube-api["`Kubernetes API Server`"] + end + + virt-operator-proxy <----> kube-api + virt-controller-proxy <----> kube-api + virt-api-proxy <----> kube-api + virt-handler-proxy <----> kube-api + + subgraph cdi ["CDI"] + subgraph cdi-operator-deploy ["`Deploy/cdi-operator`"] + cdi-operator-proxy{{"container: + proxy"}} + cdi-operator("`container: + virt-handler`") + cdi-operator --> cdi-operator-proxy + cdi-operator-proxy --> cdi-operator + end + + subgraph cdi-deployment-deploy ["`Deploy/cdi-deployment`"] + cdi-deployment-proxy{{"container: + proxy"}} + cdi-deployment("`container: + cdi-eployment`") + cdi-deployment --> cdi-deployment-proxy + cdi-deployment-proxy --> cdi-deployment + end + + subgraph cdi-api-deploy ["`Deploy/cdi-api`"] + cdi-api-proxy{{"container: + proxy"}} + cdi-api("`container: + cdi-api`") + cdi-api --> cdi-api-proxy + cdi-api-proxy --> cdi-api + end + + subgraph cdi-exportproxy-deploy ["`Deploy/cdi-exportproxy`"] + cdi-exportproxy-proxy{{"container: + proxy"}} + cdi-exportproxy("`container: + cdi-exportproxy`") + cdi-exportproxy --> cdi-exportproxy-proxy + cdi-exportproxy-proxy --> cdi-exportproxy + end + end + kube-api <----> cdi-operator-proxy + kube-api <----> cdi-deployment-proxy + kube-api <----> cdi-api-proxy + kube-api <----> cdi-exportproxy-proxy + + + subgraph d8virt ["D8 API"] + subgraph d8-virt-deploy ["Deploy/virtualization-controller"] + d8-virt-controller-proxy("`container: + proxy`") + d8-virt-controller("`container: + virtualization-controller`") + d8-virt-controller --> d8-virt-controller-proxy + d8-virt-controller-proxy --> d8-virt-controller + end + end + + kube-api <----> d8-virt-controller-proxy +``` + +Variation (block diagram seems not so powerful as flowchart) +```mermaid +block-beta + columns 5 + + %% Main containers in kubevirt Pods + virtoperator["virt-operator"] + virtapi["virt-api"] + virtcontroller["virt-controller"] + virthandler["virt-handler"] + virtexportproxy["virt-exportproxy"] + + %% Space for links. + space:5 + %% Links between containers. + virtoperator --> virtoperatorproxy + %%virtoperatorproxy --> virtoperator + virtapi --> virtapiproxy + virtcontroller --> virtcontrollerproxy + virthandler --> virthandlerproxy + virtexportproxy --> virtexportproxyproxy + + %% Proxies in kubevirt Pods. + virtoperatorproxy(["proxy"]) + virtapiproxy(["proxy"]) + virtcontrollerproxy(["proxy"]) + virthandlerproxy(["proxy"]) + virtexportproxyproxy(["proxy"]) + + space:5 + + space + kubeapiserver{{"Kubernetes API Server"}}:3 + space + + virtoperatorproxy --> kubeapiserver + %%kubeapiserver --> virtoperatorproxy + virtapiproxy --> kubeapiserver + virtcontrollerproxy --> kubeapiserver + virthandlerproxy --> kubeapiserver + virtexportproxyproxy --> kubeapiserver + + space:5 + cdioperatorproxy --> kubeapiserver + cdiapiproxy --> kubeapiserver + cdideploymentproxy --> kubeapiserver + cdiuploadproxyproxy --> kubeapiserver + virtualizationcontrollerproxy --> kubeapiserver + + %% Proxies in CDI Pods. + cdioperatorproxy(["proxy"]) + cdiapiproxy(["proxy"]) + cdideploymentproxy(["proxy"]) + cdiuploadproxyproxy(["proxy"]) + virtualizationcontrollerproxy(["proxy"]) + + %% Links inside CDI Pods. + space:5 + cdioperator --> cdioperatorproxy + cdiapi--> cdiapiproxy + cdideployment --> cdideploymentproxy + cdiuploadproxy --> cdiuploadproxyproxy + virtualizationcontroller --> virtualizationcontrollerproxy + + cdioperator["cdi-operator"] + cdiapi["cdi-api"] + cdideployment["cdi-deployment"] + cdiuploadproxy["cdi-uploadproxy"] + virtualizationcontroller["virtualization- + controller"] +``` + +### Changes to add proxy to the Pod +- Add a ConfigMap with a simple kubeconfig points to the local proxy. + ``` + ... + clusters: + - cluster: + server: http://127.0.0.1:23915 + ... + ``` +- Add a volume and a volumeMount to pass new kubeconfig as file to the main container. +- Set KUBECONFIG variable in the main container. File should contain configuration to connect to proxy port. + - Note: kubevirt containers use --kubeconfig flag, cdi containers use KUBECONFIG env variable. +- Add a new sidecar container with the proxy. + - Set WEBHOOK_ADDRESS if webhook proxying is required. + - Add volumeMount with a certificate and set WEBHOOK_CERT_FILE and WEBHOOK_KEY_FILE to use the certificate. + - Add port 24192 to the webhook Service to use the certificate without issuing new one with changed ServerName. + +## API client proxying + +Implemented rewrites: +- apiGroup, kind, metadata.ownerReferences for Kubevirt and CDI Custom Resources. +- metadata.ownerReferences for Pod +- rules for Role, ClusterRole +- webhooks[].rules for ValidatingWebhookConfiguration, MutatingWebhookConfiguration +- metadata.name, spec.group, spec.names for CustomResourceDefinition. +- patch /spec for CustomResourceDefinition. +- fieldSelector=metadata.name=&watch=true for CRD. +- request.resource, request.object, request.kind, etc. for AdmissionReview. + +TODO: +- labels and annotations for Kubevirt and CDI CRs and all kubevirt related resources, Nodes and Pods. +- patches in general. +- SubjectAccessReview https://dev-k8sref-io.web.app/docs/authorization/subjectaccessreview-v1/ + +```plantuml +@startuml +box "Pod with Controller" #fff +participant "container\nname: controller" as ctrl +note over ctrl +Use KUBECONFIG file to connect +to local proxy instead of +directly using API server: +""clusters:"" +""- cluster:"" +"" server: http://127.0.0.1:23915"" +endnote +queue "additional container\nname: proxy" as proxy +/ note over proxy +Listen on ""127.0.0.1:23915"" +and pass requests to +Kubernetes API Server +endnote +endbox +box "Control Plane" #fff +participant "Kubernetes\nAPI Server" as kube_api +endbox + +== Get, List, Delete operations == + +ctrl -> proxy : Request operation via endpoint:\n\n/apis/kubevirt.io/v1/virtualmachines +proxy -> kube_api : Rewrite endpoint, pass request to:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines + +kube_api -> proxy : Response with renamed resources:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine +proxy -> ctrl : Rewrite payload, pass\nresponse with restored resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine + +== Create, Update, Patch operations == + +ctrl -> proxy : Request operation via endpoint:\n\n/apis/kubevirt.io/v1/virtualmachines\n\nA payload contains original resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine +proxy -> kube_api : Rewrite endpoint and payload,\npass request with renamed resources:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine + +kube_api -> proxy : Response with renamed resources:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine +proxy -> ctrl : Rewrite payload, pass\nresponse with restored resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine + +== Watch operation == + +ctrl -> proxy : Request WATCH operation via endpoint:\n\n/apis/kubevirt.io↩︎\n/v1/virtualmachines?watch=true +activate proxy +proxy -> kube_api : Rewrite endpoint, pass request to:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines?watch=true +activate kube_api + +kube_api -> kube_api : Generate\nWATCH\nevents + +kube_api -> proxy : ADDED, MODIFIED or DELETED\nevent with renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine +activate proxy +proxy -> ctrl : Rewrite payload, pass\nevent with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine +deactivate proxy + +kube_api -> proxy : BOOKMARK event with renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine +activate proxy +proxy -> ctrl : Rewrite payload, pass\nevent with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine +deactivate proxy + +kube_api -> proxy : Stop WATCH operation +deactivate kube_api +proxy -> ctrl : Stop WATCH operation +deactivate proxy + +@endplantuml +``` + + +## Webhook proxying + +Kubernetes API Server connects to proxy, so proxy will pass AdmissionReview to real webhook. Proxy may rewrite JSON payloads +for different purposes, e.g. resources renaming. + +Additional changes: + +- A targetPort in the webhook Service should point to proxy container. +- A proxy container should mount secret with certificates. + +```plantuml +@startuml +box "Pod with Controller" #fff +participant "container\nname: controller" as ctrl +queue "additional container\nname: proxy" as proxy +endbox +box "Control Plane" #fff +participant "Kubernetes\nAPI Server" as kube_api +endbox + +note over ctrl +Listen on ""0.0.0.0:9443"" +endnote +/ note over proxy +Listen on ""0.0.0.0:24192"" +and pass requests to +the controller ""127.0.0.1:9443"" +endnote +/ note over kube_api +Pass AdmissionReview to Pod +endnote + +== Webhook handling == + +kube_api -> proxy : Request admission review via\nconfigured endpoint:\n\n/validate-x-virtualization-↩︎\ndeckhouse-io-prefixed-virtualmachines\n\nA payload contains renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine +proxy -> ctrl : Rewrite admission review, pass\nrequest with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine + +... Validating webhook response ... +ctrl -> proxy : AdmissionReview response +proxy -> kube_api : No rewrite, pass as-is. + +... Mutating webhook response ... +ctrl -> proxy : AdmissionReview response\nwith the patch +proxy -> kube_api : Rewrite ownerRef patch if\nresponse.patchType == JSONPatch\nand patch operates on the ownerRef content + + +@enduml +``` + +```mermaid +--- +config: + htmlLabels: false +--- + +sequenceDiagram + + box Pod with controller + participant ctrl as container
name: controller + participant proxy as container
name: proxy + end + + Note over ctrl: Listen on 0.0.0.0:9443 + Note over proxy: Listen on 0.0.0.0:24192
and pass requests to
127.0.0.1:9443 + + box Control plane + participant kubeapi as Kubernetes
API Server + end + note over kubeapi: Request webhook with AdmissionReview + + kubeapi --> ctrl: Webhook handling + + kubeapi ->>+ proxy: Send AdmissionReview with
renamed resources
apiVersion: x.virtualization.deckhouse.io
PrefixedVirtualMachine + + proxy ->>+ ctrl: Proxy restores resource:
apiGroup, kind, ownerReferences
apiVersion: kubevirt.io
kind: VirtualMachine + + ctrl ->>- proxy: AdmissionReview
with webhook response + + alt Validating webhook response + proxy ->> kubeapi: No rewrite, pass as-is + else Mutating webhook response + proxy ->>- kubeapi: Rewrite patch if
ownerReferences is modified + end + + + + %%participant Bob + %% ctrl->>John: "`This **is** _Markdown_`" + %%loop HealthCheck + %% John->>John: Fight against hypochondria + %%end + %%Note right of John: Rational thoughts
prevail! + %%John-->>ctrl: Great! + %%John->>Bob: How about you? + %%Bob-->>John: Jolly good! +``` diff --git a/images/kube-api-rewriter/Taskfile.dist.yaml b/images/kube-api-rewriter/Taskfile.dist.yaml new file mode 100644 index 0000000..cc0f0de --- /dev/null +++ b/images/kube-api-rewriter/Taskfile.dist.yaml @@ -0,0 +1,118 @@ +version: "3" + +silent: true + +includes: + my: + taskfile: Taskfile.my.yaml + optional: true + +vars: + DevImage: "${DevImage:-localhost:5000/$USER/kube-api-rewriter:latest}" + +tasks: + default: + cmds: + - task: dev:status + dev:build: + desc: "build latest image with kube-api-rewriter and test-controller" + cmds: + - | + docker build . -t {{.DevImage}} -f local/Dockerfile + docker push {{.DevImage}} + + dev:deploy: + desc: "apply manifest with kube-api-rewriter and test-controller" + cmds: + - task: dev:__deploy + vars: + CTR_COMMAND: "['./kube-api-rewriter']" + + dev:deploy-with-dlv: + desc: "apply manifest with kube-api-rewriter with dlv and test-controller" + cmds: + - task: dev:__deploy + vars: + CTR_COMMAND: "['./dlv', '--listen=:2345', '--headless=true', '--continue', '--log=true', '--log-output=debugger,debuglineerr,gdbwire,lldbout,rpc', '--accept-multiclient', '--api-version=2', 'exec', './kube-api-rewriter']" + + dev:__deploy: + internal: true + cmds: + - | + if ! kubectl get no 2>&1 >/dev/null ; then + echo Restart cluster connection + exit 1 + fi + - | + kubectl get ns kproxy &>/dev/null || kubectl create ns kproxy + kubectl apply -f - <&1 >/dev/null ; then + echo Restart cluster connection + exit 1 + fi + - | + kubectl -n kproxy scale deployment/kube-api-rewriter --replicas=0 + kubectl -n kproxy scale deployment/kube-api-rewriter --replicas=1 + + dev:redeploy: + desc: "build, deploy, restart" + cmds: + - | + if ! kubectl get no 2>&1 >/dev/null ; then + echo Restart cluster connection + exit 1 + fi + - task: dev:build + - task: dev:deploy + - task: dev:restart + - | + sleep 3 + kubectl -n kproxy get all + + dev:status: + cmds: + - | + kubectl -n kproxy get po,deploy + + dev:curl: + desc: "run curl in kube-api-rewriter deployment" + cmds: + - | + kubectl -n kproxy exec -t deploy/kube-api-rewriter -- curl {{.CLI_ARGS}} + + dev:kubectl: + desc: "run kubectl in kube-api-rewriter deployment" + cmds: + - | + kubectl -n kproxy exec deploy/kube-api-rewriter -c proxy -- kubectl -s 127.0.0.1:23915 {{.CLI_ARGS}} + #kubectl -n d8-virtualization exec deploy/virt-operator -- kubectl -s 127.0.0.1:23915 {{.CLI_ARGS}} + + logs:proxy: + desc: "Logs for proxy container" + cmds: + - | + kubectl -n kproxy logs deployments/kube-api-rewriter -c proxy -f + + logs:controller: + desc: "Logs for test-controller container" + cmds: + - | + kubectl -n kproxy logs deployments/kube-api-rewriter -c controller -f diff --git a/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go b/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go new file mode 100644 index 0000000..23d3d13 --- /dev/null +++ b/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go @@ -0,0 +1,224 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + log "log/slog" + "net/http" + "os" + + "github.com/deckhouse/kube-api-rewriter/pkg/kubevirt" + logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/healthz" + "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/metrics" + "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/profiler" + "github.com/deckhouse/kube-api-rewriter/pkg/proxy" + "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" + "github.com/deckhouse/kube-api-rewriter/pkg/server" + "github.com/deckhouse/kube-api-rewriter/pkg/target" +) + +// This proxy is a proof-of-concept of proxying Kubernetes API requests +// with rewrites. +// +// It assumes presence of KUBERNETES_* environment variables and files +// in /var/run/secrets/kubernetes.io/serviceaccount (token and ca.crt). +// +// A client behind the proxy should connect to 127.0.0.1:$PROXY_PORT +// using plain http. Example of kubeconfig file: +// apiVersion: v1 +// kind: Config +// clusters: +// - cluster: +// server: http://127.0.0.1:23915 +// name: proxy.api.server +// contexts: +// - context: +// cluster: proxy.api.server +// name: proxy.api.server +// current-context: proxy.api.server + +const ( + loopbackAddr = "127.0.0.1" + anyAddr = "0.0.0.0" + defaultAPIClientProxyPort = "23915" + defaultWebhookProxyPort = "24192" +) + +const ( + logLevelEnv = "LOG_LEVEL" + logFormatEnv = "LOG_FORMAT" + logOutputEnv = "LOG_OUTPUT" +) + +const ( + MonitoringBindAddress = "MONITORING_BIND_ADDRESS" + DefaultMonitoringBindAddress = ":9090" + PprofBindAddressEnv = "PPROF_BIND_ADDRESS" +) + +func main() { + // Set options for the default logger: level, format and output. + logutil.SetupDefaultLoggerFromEnv(logutil.Options{ + Level: os.Getenv(logLevelEnv), + Format: os.Getenv(logFormatEnv), + Output: os.Getenv(logOutputEnv), + }) + + // Load rules from file or use default kubevirt rules. + rewriteRules := kubevirt.KubevirtRewriteRules + if os.Getenv("RULES_PATH") != "" { + rulesFromFile, err := rewriter.LoadRules(os.Getenv("RULES_PATH")) + if err != nil { + log.Error("Load rules from %s: %v", os.Getenv("RULES_PATH"), err) + os.Exit(1) + } + rewriteRules = rulesFromFile + } + rewriteRules.Init() + + // Init and register metrics. + metrics.Init() + proxy.RegisterMetrics() + + httpServers := make([]*server.HTTPServer, 0) + + // Now add proxy workers with rewriters. + hasRewriter := false + + // Register direct proxy from local Kubernetes API client to Kubernetes API server. + if os.Getenv("CLIENT_PROXY") == "no" { + log.Info("Will not start client proxy: CLIENT_PROXY=no") + } else { + config, err := target.NewKubernetesTarget() + if err != nil { + log.Error("Load Kubernetes REST", logutil.SlogErr(err)) + os.Exit(1) + } + lAddr := server.ConstructListenAddr( + os.Getenv("CLIENT_PROXY_ADDRESS"), os.Getenv("CLIENT_PROXY_PORT"), + loopbackAddr, defaultAPIClientProxyPort) + rwr := &rewriter.RuleBasedRewriter{ + Rules: rewriteRules, + } + proxyHandler := &proxy.Handler{ + Name: "kube-api", + TargetClient: config.Client, + TargetURL: config.APIServerURL, + ProxyMode: proxy.ToRenamed, + Rewriter: rwr, + } + proxyHandler.Init() + proxySrv := &server.HTTPServer{ + InstanceDesc: "API Client proxy", + ListenAddr: lAddr, + RootHandler: proxyHandler, + } + httpServers = append(httpServers, proxySrv) + hasRewriter = true + } + + // Register reverse proxy from Kubernetes API server to local webhook server. + if os.Getenv("WEBHOOK_ADDRESS") == "" { + log.Info("Will not start webhook proxy for empty WEBHOOK_ADDRESS") + } else { + config, err := target.NewWebhookTarget() + if err != nil { + log.Error("Configure webhook client", logutil.SlogErr(err)) + os.Exit(1) + } + lAddr := server.ConstructListenAddr( + os.Getenv("WEBHOOK_PROXY_ADDRESS"), os.Getenv("WEBHOOK_PROXY_PORT"), + anyAddr, defaultWebhookProxyPort) + rwr := &rewriter.RuleBasedRewriter{ + Rules: rewriteRules, + } + proxyHandler := &proxy.Handler{ + Name: "webhook", + TargetClient: config.Client, + TargetURL: config.URL, + ProxyMode: proxy.ToOriginal, + Rewriter: rwr, + } + proxyHandler.Init() + proxySrv := &server.HTTPServer{ + InstanceDesc: "Webhook proxy", + ListenAddr: lAddr, + RootHandler: proxyHandler, + CertManager: config.CertManager, + } + httpServers = append(httpServers, proxySrv) + hasRewriter = true + } + + if !hasRewriter { + log.Info("No proxy rewriters to start, exit. Check CLIENT_PROXY and WEBHOOK_ADDRESS environment variables.") + return + } + + // Always add monitoring server with metrics and healthz probes + { + lAddr := os.Getenv(MonitoringBindAddress) + if lAddr == "" { + lAddr = DefaultMonitoringBindAddress + } + + monMux := http.NewServeMux() + healthz.AddHealthzHandler(monMux) + metrics.AddMetricsHandler(monMux) + + monSrv := &server.HTTPServer{ + InstanceDesc: "Monitoring handlers", + ListenAddr: lAddr, + RootHandler: monMux, + CertManager: nil, + Err: nil, + } + httpServers = append(httpServers, monSrv) + } + + // Enable pprof server if bind address is specified. + pprofBindAddress := os.Getenv(PprofBindAddressEnv) + if pprofBindAddress != "" { + pprofHandler := profiler.NewPprofHandler() + + pprofSrv := &server.HTTPServer{ + InstanceDesc: "Pprof", + ListenAddr: pprofBindAddress, + RootHandler: pprofHandler, + } + httpServers = append(httpServers, pprofSrv) + } + + // Start all registered servers and block the main process until at least one server stops. + group := server.NewRunnableGroup() + for i := range httpServers { + group.Add(httpServers[i]) + } + // Block while servers are running. + group.Start() + + // Log errors for each instance and exit. + exitCode := 0 + for _, srv := range httpServers { + if srv.Err != nil { + log.Error(srv.InstanceDesc, logutil.SlogErr(srv.Err)) + exitCode = 1 + } + } + os.Exit(exitCode) +} diff --git a/images/kube-api-rewriter/go.mod b/images/kube-api-rewriter/go.mod new file mode 100644 index 0000000..6385af8 --- /dev/null +++ b/images/kube-api-rewriter/go.mod @@ -0,0 +1,76 @@ +module github.com/deckhouse/kube-api-rewriter + +go 1.24.13 + +require ( + github.com/fsnotify/fsnotify v1.9.0 + github.com/josephburnett/jd v1.9.2 + github.com/kr/text v0.2.0 + github.com/prometheus/client_golang v1.23.0 + github.com/stretchr/testify v1.10.0 + github.com/tidwall/gjson v1.18.0 + github.com/tidwall/sjson v1.2.5 + k8s.io/api v0.33.3 + k8s.io/apimachinery v0.33.3 + k8s.io/client-go v0.33.3 + sigs.k8s.io/controller-runtime v0.21.0 + sigs.k8s.io/yaml v1.6.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect + 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/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.9.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/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/spf13/pflag v1.0.7 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // 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/net v0.42.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/time v0.12.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // 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 + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) + +replace google.golang.org/protobuf => google.golang.org/protobuf v1.33.0 + +// CVE Replaces +replace ( + golang.org/x/net => golang.org/x/net v0.40.0 // CVE-2025-22870, CVE-2025-22872 + golang.org/x/oauth2 => golang.org/x/oauth2 v0.27.0 // CVE-2025-22868 +) diff --git a/images/kube-api-rewriter/go.sum b/images/kube-api-rewriter/go.sum new file mode 100644 index 0000000..6961820 --- /dev/null +++ b/images/kube-api-rewriter/go.sum @@ -0,0 +1,216 @@ +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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +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.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/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/gofuzz v1.0.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/josephburnett/jd v1.9.2 h1:ECJRRFXCCqbtidkAHckHGSZm/JIaAxS1gygHLF8MI5Y= +github.com/josephburnett/jd v1.9.2/go.mod h1:bImDr8QXpxMb3SD+w1cDRHp97xP6UwI88xUAuxwDQfM= +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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= +github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= +github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +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= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +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= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= +golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +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= +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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= +golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= +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= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= +k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= +k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= +k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= +k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= +k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= +k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/kube-api-rewriter/local/Dockerfile b/images/kube-api-rewriter/local/Dockerfile new file mode 100644 index 0000000..d3b3ff3 --- /dev/null +++ b/images/kube-api-rewriter/local/Dockerfile @@ -0,0 +1,45 @@ +# Build kube-api-rewriter for local development purposes. +# Note: it is not a part of the production build! + +# Go builder. +FROM golang:1.22.7-alpine3.19 AS builder + +RUN go install github.com/go-delve/delve/cmd/dlv@latest + +# Cache-friendly download of go dependencies. +ADD go.mod go.sum /app/ +WORKDIR /app +RUN go mod download + +ADD . /app + +RUN GOOS=linux \ + go build -o kube-api-rewriter ./cmd/kube-api-rewriter + +# Go builder. +FROM golang:1.22.7-alpine3.19 AS builder-test-controller + +# Cache-friendly download of go dependencies. +ADD local/test-controller/go.mod local/test-controller/go.sum /app/ +WORKDIR /app +RUN go mod download + +ADD local/test-controller/main.go /app/ + +RUN GOOS=linux \ + go build -o test-controller . + +FROM alpine:3.19 +RUN apk --no-cache add ca-certificates bash sed tini curl && \ + kubectlArch=linux/amd64 && \ + echo "Download kubectl for ${kubectlArch}" && \ + wget https://storage.googleapis.com/kubernetes-release/release/v1.30.0/bin/${kubectlArch}/kubectl -O /bin/kubectl && \ + chmod +x /bin/kubectl +COPY --from=builder /go/bin/dlv / +COPY --from=builder /app/kube-api-rewriter / +COPY --from=builder-test-controller /app/test-controller / +ADD local/kube-api-rewriter.kubeconfig / + +# Use user nobody. +USER 65534:65534 +WORKDIR / diff --git a/images/kube-api-rewriter/local/kube-api-rewriter.kubeconfig b/images/kube-api-rewriter/local/kube-api-rewriter.kubeconfig new file mode 100644 index 0000000..11f4a32 --- /dev/null +++ b/images/kube-api-rewriter/local/kube-api-rewriter.kubeconfig @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Config +clusters: +- cluster: + server: http://127.0.0.1:23915 + name: kube-api-rewriter +contexts: +- context: + cluster: kube-api-rewriter + name: kube-api-rewriter +current-context: kube-api-rewriter diff --git a/images/kube-api-rewriter/local/proxy-gen-certs.sh b/images/kube-api-rewriter/local/proxy-gen-certs.sh new file mode 100755 index 0000000..9514d0d --- /dev/null +++ b/images/kube-api-rewriter/local/proxy-gen-certs.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash + +# Copyright 2024 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. + +NAMESPACE=kproxy +SERVICE_NAME=test-admission-webhook +CN="api proxying tests for validating webhook" +OUTDIR=proxy-certs + +COMMON_NAME=${SERVICE_NAME}.${NAMESPACE} + +set -eo pipefail + +echo ================================================================= +echo THIS SCRIPT IS NOT SECURE! USE IT ONLY FOR DEMONSTATION PURPOSES. +echo ================================================================= +echo + +mkdir -p ${OUTDIR} && cd ${OUTDIR} + +if [[ -e ca.csr ]] ; then + read -p "Regenerate certificates? (yes/no) [no]: " + if [[ ! $REPLY =~ ^[Yy][Ee][Ss]$ ]] + then + exit 0 + fi +fi + +RM_FILES="ca* cert*" +echo ">>> Remove ${RM_FILES}" +rm -f $RM_FILES + +echo ">>> Generate CA key and certificate" +cat <>> Generate cert.key and cert.crt" +cat < ./../../../../api + +// TODO: delete this replaces after fixing https://github.com/golang/go/issues/66403. +replace ( + github.com/cilium/proxy => github.com/cilium/proxy v0.0.0-20231202123106-38b645b854f3 + github.com/markbates/safe => github.com/markbates/safe v1.0.1 + k8s.io/api => k8s.io/api v0.29.2 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.29.2 + k8s.io/apimachinery => k8s.io/apimachinery v0.29.2 + k8s.io/apiserver => k8s.io/apiserver v0.29.2 + k8s.io/code-generator => k8s.io/code-generator v0.29.2 + k8s.io/component-base => k8s.io/component-base v0.29.2 + k8s.io/kms => k8s.io/kms v0.29.2 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/evanphx/json-patch/v5 v5.8.0 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.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/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 // indirect + github.com/openshift/custom-resource-status v1.1.2 // indirect + github.com/pborman/uuid v1.2.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.18.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/oauth2 v0.12.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect + golang.org/x/time v0.3.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/component-base v0.29.2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 // indirect + kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/images/kube-api-rewriter/local/test-controller/go.sum b/images/kube-api-rewriter/local/test-controller/go.sum new file mode 100644 index 0000000..e0ca07b --- /dev/null +++ b/images/kube-api-rewriter/local/test-controller/go.sum @@ -0,0 +1,484 @@ +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= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +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/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckhouse/virtualization/api v0.0.0-20240417135227-efb465e54575 h1:FdSicGvp9Gz1dvrzV7vVkMAlEMYUWMKq/QLKeZxZOtw= +github.com/deckhouse/virtualization/api v0.0.0-20240417135227-efb465e54575/go.mod h1:1tfoFeZmlKqq6jEuSfIpdrxsBpOcMajYaCbO94pVQLs= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.15.0+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.8.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +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/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.8.0 h1:lRj6N9Nci7MvzrXuX6HFzU8XjmhPiXPlsKEy1u0KQro= +github.com/evanphx/json-patch/v5 v5.8.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/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/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= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/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.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +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.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +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/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-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +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.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= +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/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +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/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +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/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= +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/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.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +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/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +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.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.9.0/go.mod h1:4xkjoL/tZv4SMWeww56BU5kAt19mVB47gTWxmrTcxyk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= +github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= +github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/ginkgo/v2 v2.14.0 h1:vSmGj2Z5YPb9JwCWT6z6ihcUvDhuXLc3sJiqd3jMKAY= +github.com/onsi/ginkgo/v2 v2.14.0/go.mod h1:JkUdW7JkN0V6rFvsHcJ478egV3XH9NxpD27Hal/PhZw= +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.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.1/go.mod h1:aHX5xOykVYzWOV4WqQy0sy8BQptgukenXpCXfadcIAw= +github.com/onsi/gomega v1.27.3/go.mod h1:5vG284IBtfDAmDyrK+eGyZmUgUlmi+Wngqo557cZ6Gw= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= +github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8= +github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183 h1:t/CahSnpqY46sQR01SoS+Jt0jtjgmhgE6lFmRnO4q70= +github.com/openshift/api v0.0.0-20230503133300-8bbcb7ca7183/go.mod h1:4VWG+W22wrB4HfBL88P40DxLEpSOaiBVxUnfalfJo9k= +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/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw= +github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/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.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.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/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.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +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.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= +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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +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.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= +golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/oauth2 v0.12.0 h1:smVPGxink+n1ZI5pkQa8y6fZT0RW0MgCO5bFpepy4B4= +golang.org/x/oauth2 v0.12.0/go.mod h1:A74bZ3aGXgCY0qaIC9Ahg6Lglin4AMAco8cIv9baba4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +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.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= +golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA= +golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0= +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= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +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/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +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= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apiextensions-apiserver v0.29.2 h1:UK3xB5lOWSnhaCk0RFZ0LUacPZz9RY4wi/yt2Iu+btg= +k8s.io/apiextensions-apiserver v0.29.2/go.mod h1:aLfYjpA5p3OwtqNXQFkhJ56TB+spV8Gc4wfMhUA3/b8= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= +k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/code-generator v0.29.2/go.mod h1:FwFi3C9jCrmbPjekhaCYcYG1n07CYiW1+PAPCockaos= +k8s.io/component-base v0.29.2 h1:lpiLyuvPA9yV1aQwGLENYyK7n/8t6l3nn3zAtFTJYe8= +k8s.io/component-base v0.29.2/go.mod h1:BfB3SLrefbZXiBfbM+2H1dlat21Uewg/5qtKOl8degM= +k8s.io/gengo v0.0.0-20210813121822-485abfe95c7c/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20211129171323-c02415ce4185/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.40.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.80.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20220124234850-424119656bbf/go.mod h1:sX9MT8g7NVZM5lVL/j8QyCCJe8YSMW30QvGZWaCIDIk= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20210802155522-efc7438f0176/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +kubevirt.io/api v1.0.0 h1:RBdXP5CDhE0v5qL2OUQdrYyRrHe/F68Z91GWqBDF6nw= +kubevirt.io/api v1.0.0/go.mod h1:CJ4vZsaWhVN3jNbyc9y3lIZhw8nUHbWjap0xHABQiqc= +kubevirt.io/containerized-data-importer-api v1.57.0-alpha1 h1:IWo12+ei3jltSN5jQN1xjgakfvRSF3G3Rr4GXVOOy2I= +kubevirt.io/containerized-data-importer-api v1.57.0-alpha1/go.mod h1:Y/8ETgHS1GjO89bl682DPtQOYEU/1ctPFBz6Sjxm4DM= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90 h1:QMrd0nKP0BGbnxTqakhDZAUhGKxPiPiN5gSDqKUmGGc= +kubevirt.io/controller-lifecycle-operator-sdk/api v0.0.0-20220329064328-f3cc58c6ed90/go.mod h1:018lASpFYBsYN6XwmA2TIrPCx6e0gviTd/ZNtSitKgc= +sigs.k8s.io/controller-runtime v0.17.2 h1:FwHwD1CTUemg0pW2otk7/U5/i5m2ymzvOXdbeGOUvw0= +sigs.k8s.io/controller-runtime v0.17.2/go.mod h1:+MngTvIQQQhfXtwfdGw/UOQ/aIaqsYywfCINOtwMO/s= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +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/images/kube-api-rewriter/local/test-controller/main.go b/images/kube-api-rewriter/local/test-controller/main.go new file mode 100644 index 0000000..f602da2 --- /dev/null +++ b/images/kube-api-rewriter/local/test-controller/main.go @@ -0,0 +1,369 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "flag" + "fmt" + "os" + "runtime" + "strconv" + + "github.com/deckhouse/virtualization/api/core/v1alpha2" + "github.com/go-logr/logr" + "go.uber.org/zap/zapcore" + corev1 "k8s.io/api/core/v1" + extv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apiruntime "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + virtv1 "kubevirt.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/manager/signals" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var ( + log = logf.Log.WithName("cmd") + resourcesSchemeFuncs = []func(*apiruntime.Scheme) error{ + clientgoscheme.AddToScheme, + extv1.AddToScheme, + virtv1.AddToScheme, + v1alpha2.AddToScheme, + } +) + +const ( + podNamespaceVar = "POD_NAMESPACE" + defaultVerbosity = "1" +) + +func setupLogger() { + verbose := defaultVerbosity + if verboseEnvVarVal := os.Getenv("VERBOSITY"); verboseEnvVarVal != "" { + verbose = verboseEnvVarVal + } + // visit actual flags passed in and if passed check -v and set verbose + if fv := flag.Lookup("v"); fv != nil { + verbose = fv.Value.String() + } + if verbose == defaultVerbosity { + log.V(1).Info(fmt.Sprintf("Note: increase the -v level in the controller deployment for more detailed logging, eg. -v=%d or -v=%d\n", 2, 3)) + } + verbosityLevel, err := strconv.Atoi(verbose) + debug := false + if err == nil && verbosityLevel > 1 { + debug = true + } + + // The logger instantiated here can be changed to any logger + // implementing the logr.Logger interface. This logger will + // be propagated through the whole operator, generating + // uniform and structured logs. + logf.SetLogger(zap.New(zap.Level(zapcore.Level(-1*verbosityLevel)), zap.UseDevMode(debug))) +} + +func printVersion() { + log.Info(fmt.Sprintf("Go Version: %s", runtime.Version())) + log.Info(fmt.Sprintf("Go OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH)) +} + +func main() { + flag.Parse() + + setupLogger() + printVersion() + + // Get a config to talk to the apiserver + cfg, err := config.GetConfig() + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + leaderElectionNS := os.Getenv(podNamespaceVar) + if leaderElectionNS == "" { + leaderElectionNS = "default" + } + + // Setup scheme for all resources + scheme := apiruntime.NewScheme() + for _, f := range resourcesSchemeFuncs { + err = f(scheme) + if err != nil { + log.Error(err, "Failed to add to scheme") + os.Exit(1) + } + } + + managerOpts := manager.Options{ + // This controller watches resources in all namespaces. + LeaderElection: false, + LeaderElectionNamespace: leaderElectionNS, + LeaderElectionID: "test-controller-leader-election-helper", + LeaderElectionResourceLock: "leases", + Scheme: scheme, + } + + // Create a new Manager to provide shared dependencies and start components + mgr, err := manager.New(cfg, managerOpts) + if err != nil { + log.Error(err, "") + os.Exit(1) + } + + log.Info("Bootstrapping the Manager.") + + // Setup context to gracefully handle termination. + ctx := signals.SetupSignalHandler() + + // Add initial lister to sync rules and routes at start. + initLister := &InitialLister{ + client: mgr.GetClient(), + log: log, + } + err = mgr.Add(initLister) + if err != nil { + log.Error(err, "add initial lister to the manager") + } + + // + if _, err := NewController(ctx, mgr, log); err != nil { + log.Error(err, "") + os.Exit(1) + } + + // Start the Manager. + if err := mgr.Start(ctx); err != nil { + log.Error(err, "manager exited non-zero") + os.Exit(1) + } +} + +// InitialLister is a Runnable implementatin to access existing objects +// before handling any event with Reconcile method. +type InitialLister struct { + log logr.Logger + client client.Client +} + +func (i *InitialLister) Start(ctx context.Context) error { + cl := i.client + + // List VMs, Pods, CRDs before starting manager. + vms := v1alpha2.VirtualMachineList{} + err := cl.List(ctx, &vms) + if err != nil { + i.log.Error(err, "list VMs") + return err + } + log.Info(fmt.Sprintf("List returns %d VMs", len(vms.Items))) + for _, vm := range vms.Items { + i.log.Info(fmt.Sprintf("observe VM %s/%s at start", vm.GetNamespace(), vm.GetName())) + } + + pods := corev1.PodList{} + err = cl.List(ctx, &pods, client.InNamespace("")) + if err != nil { + i.log.Error(err, "list Pods") + return err + } + log.Info(fmt.Sprintf("List returns %d Pods", len(pods.Items))) + for _, pod := range pods.Items { + i.log.Info(fmt.Sprintf("observe Pod %s/%s at start", pod.GetNamespace(), pod.GetName())) + } + + crds := extv1.CustomResourceDefinitionList{} + err = cl.List(ctx, &crds, client.InNamespace("")) + if err != nil { + i.log.Error(err, "list Pods") + return err + } + log.Info(fmt.Sprintf("List returns %d CRDs", len(crds.Items))) + for _, crd := range crds.Items { + i.log.Info(fmt.Sprintf("observe CRD %s/%s at start", crd.GetNamespace(), crd.GetName())) + } + + i.log.Info("Initial listing done, proceed to manager Start") + return nil +} + +const ( + controllerName = "test-controller" +) + +func NewController( + ctx context.Context, + mgr manager.Manager, + log logr.Logger, +) (controller.Controller, error) { + reconciler := &VMReconciler{ + Client: mgr.GetClient(), + Cache: mgr.GetCache(), + Recorder: mgr.GetEventRecorderFor(controllerName), + Scheme: mgr.GetScheme(), + Log: log, + } + + c, err := controller.New(controllerName, mgr, controller.Options{Reconciler: reconciler}) + if err != nil { + return nil, err + } + + if err = SetupWatches(ctx, mgr, c, log); err != nil { + return nil, err + } + + if err = SetupWebhooks(ctx, mgr, reconciler); err != nil { + return nil, err + } + + log.Info("Initialized controller with test watches") + return c, nil +} + +// SetupWatches subscripts controller to Pods, CRDs and DVP VMs. +func SetupWatches(ctx context.Context, mgr manager.Manager, ctr controller.Controller, log logr.Logger) error { + if err := ctr.Watch(source.Kind(mgr.GetCache(), &v1alpha2.VirtualMachine{}), &handler.EnqueueRequestForObject{}, + // if err := ctr.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), &handler.EnqueueRequestForObject{}, + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + log.Info(fmt.Sprintf("Got CREATE event for VM %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + log.Info(fmt.Sprintf("Got DELETE event for VM %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + log.Info(fmt.Sprintf("Got UPDATE event for VM %s/%s gvk %v", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName(), e.ObjectNew.GetObjectKind().GroupVersionKind())) + return true + }, + }, + ); err != nil { + return fmt.Errorf("error setting watch on DVP VMs: %w", err) + } + + if err := ctr.Watch(source.Kind(mgr.GetCache(), &corev1.Pod{}), &handler.EnqueueRequestForObject{}, + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + log.Info(fmt.Sprintf("Got CREATE event for Pod %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + log.Info(fmt.Sprintf("Got DELETE event for Pod %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + log.Info(fmt.Sprintf("Got UPDATE event for Pod %s/%s gvk %v", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName(), e.ObjectNew.GetObjectKind().GroupVersionKind())) + return true + }, + }, + ); err != nil { + return fmt.Errorf("error setting watch on Pods: %w", err) + } + + if err := ctr.Watch(source.Kind(mgr.GetCache(), &extv1.CustomResourceDefinition{}), &handler.EnqueueRequestForObject{}, + predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + log.Info(fmt.Sprintf("Got CREATE event for CRD %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + DeleteFunc: func(e event.DeleteEvent) bool { + log.Info(fmt.Sprintf("Got DELETE event for CRD %s/%s gvk %v", e.Object.GetNamespace(), e.Object.GetName(), e.Object.GetObjectKind().GroupVersionKind())) + return true + }, + UpdateFunc: func(e event.UpdateEvent) bool { + log.Info(fmt.Sprintf("Got UPDATE event for CRD %s/%s gvk %v", e.ObjectNew.GetNamespace(), e.ObjectNew.GetName(), e.ObjectNew.GetObjectKind().GroupVersionKind())) + return true + }, + }, + ); err != nil { + return fmt.Errorf("error setting watch on CRDs: %w", err) + } + + return nil +} + +func SetupWebhooks(ctx context.Context, mgr manager.Manager, validator admission.CustomValidator) error { + return builder.WebhookManagedBy(mgr). + For(&virtv1.VirtualMachine{}). + WithValidator(validator). + Complete() +} + +type VMReconciler struct { + Client client.Client + Cache cache.Cache + Recorder record.EventRecorder + Scheme *apiruntime.Scheme + Log logr.Logger +} + +func (r *VMReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + r.Log.Info(fmt.Sprintf("Got request for %s", req.String())) + return reconcile.Result{}, nil +} + +func (r *VMReconciler) ValidateCreate(ctx context.Context, obj apiruntime.Object) (admission.Warnings, error) { + vm, ok := obj.(*virtv1.VirtualMachine) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachine but got a %T", obj) + } + + warnings := admission.Warnings{ + fmt.Sprintf("Validate new VM %s is OK, got kind %s, apiVersion %s", vm.GetName(), vm.GetObjectKind(), vm.APIVersion), + } + return warnings, nil +} + +func (r *VMReconciler) ValidateUpdate(ctx context.Context, _, newObj apiruntime.Object) (admission.Warnings, error) { + vm, ok := newObj.(*virtv1.VirtualMachine) + if !ok { + return nil, fmt.Errorf("expected a new VirtualMachine but got a %T", newObj) + } + + warnings := admission.Warnings{ + fmt.Sprintf("Validate updated VM %s is OK, got kind %s, apiVersion %s", vm.GetName(), vm.GetObjectKind(), vm.APIVersion), + } + return warnings, nil +} + +func (v *VMReconciler) ValidateDelete(_ context.Context, obj apiruntime.Object) (admission.Warnings, error) { + vm, ok := obj.(*virtv1.VirtualMachine) + if !ok { + return nil, fmt.Errorf("expected a deleted VirtualMachine but got a %T", obj) + } + + warnings := admission.Warnings{ + fmt.Sprintf("Validate deleted VM %s is OK, got kind %s, apiVersion %s", vm.GetName(), vm.GetObjectKind(), vm.APIVersion), + } + return warnings, nil +} diff --git a/images/kube-api-rewriter/mount-points.yaml b/images/kube-api-rewriter/mount-points.yaml new file mode 100644 index 0000000..fa5ef6d --- /dev/null +++ b/images/kube-api-rewriter/mount-points.yaml @@ -0,0 +1,7 @@ +# A list of pre-created mount points for containerd strict mode. + +dirs: + - /etc/virt-operator/certificates + - /etc/virt-api/certificates + # Create dirs in /run, as /var/run is a symlink to /run. + - /run/certs/cdi-apiserver-server-cert diff --git a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go b/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go new file mode 100644 index 0000000..cc89f3c --- /dev/null +++ b/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go @@ -0,0 +1,698 @@ +/* +Copyright 2024 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 kubevirt + +import ( + . "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" +) + +const ( + internalPrefix = "internal.virtualization.deckhouse.io" + nodePrefix = "node.virtualization.deckhouse.io" + rootPrefix = "virtualization.deckhouse.io" +) + +var KubevirtRewriteRules = &RewriteRules{ + KindPrefix: "InternalVirtualization", // VirtualMachine -> InternalVirtualizationVirtualMachine + ResourceTypePrefix: "internalvirtualization", // virtualmachines -> internalvirtualizationvirtualmachines + ShortNamePrefix: "intvirt", // kubectl get intvirtvm + Categories: []string{"intvirt"}, // kubectl get intvirt to see all KubeVirt and CDI resources. + Rules: KubevirtAPIGroupsRules, + Webhooks: KubevirtWebhooks, + Labels: MetadataReplace{ + Names: []MetadataReplaceRule{ + {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, + {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, + {Original: "operator.kubevirt.io", Renamed: "operator.kubevirt." + internalPrefix}, + {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, + {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, + // Special cases. + {Original: "node-labeller.kubevirt.io/skip-node", Renamed: "node-labeller." + rootPrefix + "/skip-node"}, + {Original: "node-labeller.kubevirt.io/obsolete-host-model", Renamed: "node-labeller." + internalPrefix + "/obsolete-host-model"}, + { + Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-operator", + Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-operator-internal-virtualization", + }, + { + Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-controller", + Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-controller-internal-virtualization", + }, + { + Original: "app.kubernetes.io/managed-by", OriginalValue: "virt-operator", + Renamed: "app.kubernetes.io/managed-by", RenamedValue: "virt-operator-internal-virtualization", + }, + { + Original: "app.kubernetes.io/managed-by", OriginalValue: "kubevirt-operator", + Renamed: "app.kubernetes.io/managed-by", RenamedValue: "kubevirt-operator-internal-virtualization", + }, + }, + Prefixes: []MetadataReplaceRule{ + // CDI related labels. + {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, + {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, + {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, + {Original: "upload.cdi.kubevirt.io", Renamed: "upload.cdi." + internalPrefix}, + // KubeVirt related labels. + {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, + {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, + {Original: "operator.kubevirt.io", Renamed: "operator.kubevirt." + internalPrefix}, + {Original: "vm.kubevirt.io", Renamed: "vm.kubevirt." + internalPrefix}, + // Node features related labels. + // Note: these labels are not "internal". + {Original: "cpu-feature.node.kubevirt.io", Renamed: "cpu-feature." + nodePrefix}, + {Original: "cpu-model-migration.node.kubevirt.io", Renamed: "cpu-model-migration." + nodePrefix}, + {Original: "cpu-model.node.kubevirt.io", Renamed: "cpu-model." + nodePrefix}, + {Original: "cpu-timer.node.kubevirt.io", Renamed: "cpu-timer." + nodePrefix}, + {Original: "cpu-vendor.node.kubevirt.io", Renamed: "cpu-vendor." + nodePrefix}, + {Original: "scheduling.node.kubevirt.io", Renamed: "scheduling." + nodePrefix}, + {Original: "host-model-cpu.node.kubevirt.io", Renamed: "host-model-cpu." + nodePrefix}, + {Original: "host-model-required-features.node.kubevirt.io", Renamed: "host-model-required-features." + nodePrefix}, + {Original: "hyperv.node.kubevirt.io", Renamed: "hyperv." + nodePrefix}, + {Original: "machine-type.node.kubevirt.io", Renamed: "machine-type." + nodePrefix}, + }, + }, + Annotations: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + // CDI related annotations. + {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, + {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, + // KubeVirt related annotations. + {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, + {Original: "certificates.kubevirt.io", Renamed: "certificates.kubevirt." + internalPrefix}, + }, + }, + Finalizers: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, + {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, + }, + }, + Excludes: []ExcludeRule{ + ExcludeRule{ + Kinds: []string{ + "PersistentVolumeClaim", + "PersistentVolume", + "Pod", + }, + MatchLabels: map[string]string{ + "app.kubernetes.io/managed-by": "cdi-controller", + }, + }, + ExcludeRule{ + Kinds: []string{ + "CDI", + }, + MatchNames: []string{ + "cdi", + }, + }, + }, +} + +// TODO create generator in golang to produce below rules from Kubevirt and CDI sources so proxy can work with future versions. + +var KubevirtAPIGroupsRules = map[string]APIGroupRule{ + "cdi.kubevirt.io": { + GroupRule: GroupRule{ + Group: "cdi.kubevirt.io", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Renamed: "cdi." + internalPrefix, + }, + ResourceRules: map[string]ResourceRule{ + // cdiconfigs.cdi.kubevirt.io + "cdiconfigs": { + Kind: "CDIConfig", + ListKind: "CDIConfigList", + Plural: "cdiconfigs", + Singular: "cdiconfig", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{}, + }, + // cdis.cdi.kubevirt.io + "cdis": { + Kind: "CDI", + ListKind: "CDIList", + Plural: "cdis", + Singular: "cdi", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{"cdi", "cdis"}, + }, + // dataimportcrons.cdi.kubevirt.io + "dataimportcrons": { + Kind: "DataImportCron", + ListKind: "DataImportCronList", + Plural: "dataimportcrons", + Singular: "dataimportcron", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{"all"}, + ShortNames: []string{"dic", "dics"}, + }, + // datasources.cdi.kubevirt.io + "datasources": { + Kind: "DataSource", + ListKind: "DataSourceList", + Plural: "datasources", + Singular: "datasource", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{"all"}, + ShortNames: []string{"das"}, + }, + // datavolumes.cdi.kubevirt.io + "datavolumes": { + Kind: "DataVolume", + ListKind: "DataVolumeList", + Plural: "datavolumes", + Singular: "datavolume", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{"all"}, + ShortNames: []string{"dv", "dvs"}, + }, + // objecttransfers.cdi.kubevirt.io + "objecttransfers": { + Kind: "ObjectTransfer", + ListKind: "ObjectTransferList", + Plural: "objecttransfers", + Singular: "objecttransfer", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{"ot", "ots"}, + }, + // storageprofiles.cdi.kubevirt.io + "storageprofiles": { + Kind: "StorageProfile", + ListKind: "StorageProfileList", + Plural: "storageprofiles", + Singular: "storageprofile", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{}, + }, + // volumeclonesources.cdi.kubevirt.io + "volumeclonesources": { + Kind: "VolumeCloneSource", + ListKind: "VolumeCloneSourceList", + Plural: "volumeclonesources", + Singular: "volumeclonesource", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{}, + }, + // volumeimportsources.cdi.kubevirt.io + "volumeimportsources": { + Kind: "VolumeImportSource", + ListKind: "VolumeImportSourceList", + Plural: "volumeimportsources", + Singular: "volumeimportsource", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{}, + }, + // volumeuploadsources.cdi.kubevirt.io + "volumeuploadsources": { + Kind: "VolumeUploadSource", + ListKind: "VolumeUploadSourceList", + Plural: "volumeuploadsources", + Singular: "volumeuploadsource", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Categories: []string{}, + ShortNames: []string{}, + }, + }, + }, + "forklift.cdi.kubevirt.io": { + GroupRule: GroupRule{ + Group: "forklift.cdi.kubevirt.io", + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + Renamed: "forklift.cdi." + internalPrefix, + }, + ResourceRules: map[string]ResourceRule{ + // openstackvolumepopulators.forklift.cdi.kubevirt.io + "openstackvolumepopulators": { + Kind: "OpenstackVolumePopulator", + ListKind: "OpenstackVolumePopulatorList", + Plural: "openstackvolumepopulators", + Singular: "openstackvolumepopulator", + ShortNames: []string{"osvp", "osvps"}, + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + }, + // ovirtvolumepopulators.forklift.cdi.kubevirt.io + "ovirtvolumepopulators": { + Kind: "OvirtVolumePopulator", + ListKind: "OvirtVolumePopulatorList", + Plural: "ovirtvolumepopulators", + Singular: "ovirtvolumepopulator", + ShortNames: []string{"ovvp", "ovvps"}, + Versions: []string{"v1beta1"}, + PreferredVersion: "v1beta1", + }, + }, + }, + "kubevirt.io": { + GroupRule: GroupRule{ + Group: "kubevirt.io", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Renamed: "internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // kubevirts.kubevirt.io + "kubevirts": { + Kind: "KubeVirt", + ListKind: "KubeVirtList", + Plural: "kubevirts", + Singular: "kubevirt", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"kv", "kvs"}, + }, + // virtualmachines.kubevirt.io + "virtualmachines": { + Kind: "VirtualMachine", + ListKind: "VirtualMachineList", + Plural: "virtualmachines", + Singular: "virtualmachine", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"vm", "vms"}, + }, + // virtualmachineinstances.kubevirt.io + "virtualmachineinstances": { + Kind: "VirtualMachineInstance", + ListKind: "VirtualMachineInstanceList", + Plural: "virtualmachineinstances", + Singular: "virtualmachineinstance", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"vmi", "vmsi"}, + }, + // virtualmachineinstancemigrations.kubevirt.io + "virtualmachineinstancemigrations": { + Kind: "VirtualMachineInstanceMigration", + ListKind: "VirtualMachineInstanceMigrationList", + Plural: "virtualmachineinstancemigrations", + Singular: "virtualmachineinstancemigration", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"vmim", "vmims"}, + }, + // virtualmachineinstancepresets.kubevirt.io + "virtualmachineinstancepresets": { + Kind: "VirtualMachineInstancePreset", + ListKind: "VirtualMachineInstancePresetList", + Plural: "virtualmachineinstancepresets", + Singular: "virtualmachineinstancepreset", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"vmipreset", "vmipresets"}, + }, + // virtualmachineinstancereplicasets.kubevirt.io + "virtualmachineinstancereplicasets": { + Kind: "VirtualMachineInstanceReplicaSet", + ListKind: "VirtualMachineInstanceReplicaSetList", + Plural: "virtualmachineinstancereplicasets", + Singular: "virtualmachineinstancereplicaset", + Versions: []string{"v1", "v1alpha3"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"vmirs", "vmirss"}, + }, + }, + }, + "clone.kubevirt.io": { + GroupRule: GroupRule{ + Group: "clone.kubevirt.io", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Renamed: "clone.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // virtualmachineclones.clone.kubevirt.io + "virtualmachineclones": { + Kind: "VirtualMachineClone", + ListKind: "VirtualMachineCloneList", + Plural: "virtualmachineclones", + Singular: "virtualmachineclone", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmclone", "vmclones"}, + }, + }, + }, + "export.kubevirt.io": { + GroupRule: GroupRule{ + Group: "export.kubevirt.io", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Renamed: "export.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // virtualmachineexports.export.kubevirt.io + "virtualmachineexports": { + Kind: "VirtualMachineExport", + ListKind: "VirtualMachineExportList", + Plural: "virtualmachineexports", + Singular: "virtualmachineexport", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmexport", "vmexports"}, + }, + }, + }, + "instancetype.kubevirt.io": { + GroupRule: GroupRule{ + Group: "instancetype.kubevirt.io", + Versions: []string{"v1alpha1", "v1alpha2"}, + PreferredVersion: "v1alpha2", + Renamed: "instancetype.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // virtualmachineinstancetypes.instancetype.kubevirt.io + "virtualmachineinstancetypes": { + Kind: "VirtualMachineInstancetype", + ListKind: "VirtualMachineInstancetypeList", + Plural: "virtualmachineinstancetypes", + Singular: "virtualmachineinstancetype", + Versions: []string{"v1alpha1", "v1alpha2"}, + PreferredVersion: "v1alpha2", + Categories: []string{"all"}, + ShortNames: []string{"vminstancetype", "vminstancetypes", "vmf", "vmfs"}, + }, + // virtualmachinepreferences.instancetype.kubevirt.io + "virtualmachinepreferences": { + Kind: "VirtualMachinePreference", + ListKind: "VirtualMachinePreferenceList", + Plural: "virtualmachinepreferences", + Singular: "virtualmachinepreference", + Versions: []string{"v1alpha1", "v1alpha2"}, + PreferredVersion: "v1alpha2", + Categories: []string{"all"}, + ShortNames: []string{"vmpref", "vmprefs", "vmp", "vmps"}, + }, + // virtualmachineclusterinstancetypes.instancetype.kubevirt.io + "virtualmachineclusterinstancetypes": { + Kind: "VirtualMachineClusterInstancetype", + ListKind: "VirtualMachineClusterInstancetypeList", + Plural: "virtualmachineclusterinstancetypes", + Singular: "virtualmachineclusterinstancetype", + Versions: []string{"v1alpha1", "v1alpha2"}, + PreferredVersion: "v1alpha2", + Categories: []string{}, + ShortNames: []string{"vmclusterinstancetype", "vmclusterinstancetypes", "vmcf", "vmcfs"}, + }, + // virtualmachineclusterpreferences.instancetype.kubevirt.io + "virtualmachineclusterpreferences": { + Kind: "VirtualMachineClusterPreference", + ListKind: "VirtualMachineClusterPreferenceList", + Plural: "virtualmachineclusterpreferences", + Singular: "virtualmachineclusterpreference", + Versions: []string{"v1alpha1", "v1alpha2"}, + PreferredVersion: "v1alpha2", + Categories: []string{}, + ShortNames: []string{"vmcp", "vmcps"}, + }, + }, + }, + "migrations.kubevirt.io": { + GroupRule: GroupRule{ + Group: "migrations.kubevirt.io", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Renamed: "migrations.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // migrationpolicies.migrations.kubevirt.io + "migrationpolicies": { + Kind: "MigrationPolicy", + ListKind: "MigrationPolicyList", + Plural: "migrationpolicies", + Singular: "migrationpolicy", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{}, + }, + }, + }, + "pool.kubevirt.io": { + GroupRule: GroupRule{ + Group: "pool.kubevirt.io", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Renamed: "pool.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // virtualmachinepools.pool.kubevirt.io + "virtualmachinepools": { + Kind: "VirtualMachinePool", + ListKind: "VirtualMachinePoolList", + Plural: "virtualmachinepools", + Singular: "virtualmachinepool", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmpool", "vmpools"}, + }, + }, + }, + "snapshot.kubevirt.io": { + GroupRule: GroupRule{ + Group: "snapshot.kubevirt.io", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Renamed: "snapshot.internal.virtualization.deckhouse.io", + }, + ResourceRules: map[string]ResourceRule{ + // virtualmachinerestores.snapshot.kubevirt.io + "virtualmachinerestores": { + Kind: "VirtualMachineRestore", + ListKind: "VirtualMachineRestoreList", + Plural: "virtualmachinerestores", + Singular: "virtualmachinerestore", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmrestore", "vmrestores"}, + }, + // virtualmachinesnapshotcontents.snapshot.kubevirt.io + "virtualmachinesnapshotcontents": { + Kind: "VirtualMachineSnapshotContent", + ListKind: "VirtualMachineSnapshotContentList", + Plural: "virtualmachinesnapshotcontents", + Singular: "virtualmachinesnapshotcontent", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmsnapshotcontent", "vmsnapshotcontents"}, + }, + // virtualmachinesnapshots.snapshot.kubevirt.io + "virtualmachinesnapshots": { + Kind: "VirtualMachineSnapshot", + ListKind: "VirtualMachineSnapshotList", + Plural: "virtualmachinesnapshots", + Singular: "virtualmachinesnapshot", + Versions: []string{"v1alpha1"}, + PreferredVersion: "v1alpha1", + Categories: []string{"all"}, + ShortNames: []string{"vmsnapshot", "vmsnapshots"}, + }, + }, + }, +} + +var KubevirtWebhooks = map[string]WebhookRule{ + // CDI webhooks. + // Run this in original CDI installation: + // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l cdi.kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' -r + // TODO create generator in golang to extract these rules from resource definitions in the cdi-operator package. + "/datavolume-mutate": { + Path: "/datavolume-mutate", + Group: "cdi.kubevirt.io", + Resource: "datavolumes", + }, + "/dataimportcron-validate": { + Path: "/dataimportcron-validate", + Group: "cdi.kubevirt.io", + Resource: "dataimportcrons", + }, + "/datavolume-validate": { + Path: "/datavolume-validate", + Group: "cdi.kubevirt.io", + Resource: "datavolumes", + }, + "/cdi-validate": { + Path: "/cdi-validate", + Group: "cdi.kubevirt.io", + Resource: "cdis", + }, + "/objecttransfer-validate": { + Path: "/objecttransfer-validate", + Group: "cdi.kubevirt.io", + Resource: "objecttransfers", + }, + "/populator-validate": { + Path: "/populator-validate", + Group: "cdi.kubevirt.io", + Resource: "volumeimportsources", // Also, volumeuploadsources. This field for logging only. + }, + + // Kubevirt webhooks. + // Run this in original Kubevirt installation: + // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' + // TODO create generator in golang to extract these rules from resource definitions in the virt-operator package. + "/virtualmachineinstances-validate-create": { + Path: "/virtualmachineinstances-validate-create", + Group: "kubevirt.io", + Resource: "virtualmachineinstances", + }, + "/virtualmachineinstances-validate-update": { + Path: "/virtualmachineinstances-validate-update", + Group: "kubevirt.io", + Resource: "virtualmachineinstances", + }, + "/virtualmachines-validate": { + Path: "/virtualmachines-validate", + Group: "kubevirt.io", + Resource: "virtualmachines", + }, + "/virtualmachinereplicaset-validate": { + Path: "/virtualmachinereplicaset-validate", + Group: "kubevirt.io", + Resource: "virtualmachineinstancereplicasets", + }, + "/virtualmachinepool-validate": { + Path: "/virtualmachinepool-validate", + Group: "pool.kubevirt.io", + Resource: "virtualmachinepools", + }, + "/vmipreset-validate": { + Path: "/vmipreset-validate", + Group: "kubevirt.io", + Resource: "virtualmachineinstancepresets", + }, + "/migration-validate-create": { + Path: "/migration-validate-create", + Group: "kubevirt.io", + Resource: "virtualmachineinstancemigrations", + }, + "/migration-validate-update": { + Path: "/migration-validate-update", + Group: "kubevirt.io", + Resource: "virtualmachineinstancemigrations", + }, + "/virtualmachinesnapshots-validate": { + Path: "/virtualmachinesnapshots-validate", + Group: "snapshot.kubevirt.io", + Resource: "virtualmachinesnapshots", + }, + "/virtualmachinerestores-validate": { + Path: "/virtualmachinerestores-validate", + Group: "snapshot.kubevirt.io", + Resource: "virtualmachinerestores", + }, + "/virtualmachineexports-validate": { + Path: "/virtualmachineexports-validate", + Group: "export.kubevirt.io", + Resource: "virtualmachineexports", + }, + "/virtualmachineinstancetypes-validate": { + Path: "/virtualmachineinstancetypes-validate", + Group: "instancetype.kubevirt.io", + Resource: "virtualmachineinstancetypes", + }, + "/virtualmachineclusterinstancetypes-validate": { + Path: "/virtualmachineclusterinstancetypes-validate", + Group: "instancetype.kubevirt.io", + Resource: "virtualmachineclusterinstancetypes", + }, + "/virtualmachinepreferences-validate": { + Path: "/virtualmachinepreferences-validate", + Group: "instancetype.kubevirt.io", + Resource: "virtualmachinepreferences", + }, + "/virtualmachineclusterpreferences-validate": { + Path: "/virtualmachineclusterpreferences-validate", + Group: "instancetype.kubevirt.io", + Resource: "virtualmachineclusterpreferences", + }, + "/status-validate": { + Path: "/status-validate", + Group: "kubevirt.io", + Resource: "virtualmachines/status,virtualmachineinstancereplicasets/status,virtualmachineinstancemigrations/status", + }, + "/migration-policy-validate-create": { + Path: "/migration-policy-validate-create", + Group: "migrations.kubevirt.io", + Resource: "migrationpolicies", + }, + "/vm-clone-validate-create": { + Path: "/vm-clone-validate-create", + Group: "clone.kubevirt.io", + Resource: "virtualmachineclones", + }, + "/kubevirt-validate-delete": { + Path: "/kubevirt-validate-delete", + Group: "kubevirt.io", + Resource: "kubevirts", + }, + "/kubevirt-validate-update": { + Path: "/kubevirt-validate-update", + Group: "kubevirt.io", + Resource: "kubevirts", + }, + "/virtualmachines-mutate": { + Path: "/virtualmachines-mutate", + Group: "kubevirt.io", + Resource: "virtualmachines", + }, + "/virtualmachineinstances-mutate": { + Path: "/virtualmachineinstances-mutate", + Group: "kubevirt.io", + Resource: "virtualmachineinstances", + }, + "/migration-mutate-create": { + Path: "/migration-mutate-create", + Group: "kubevirt.io", + Resource: "virtualmachineinstancemigrations", + }, + "/vm-clone-mutate-create": { + Path: "/vm-clone-mutate-create", + Group: "clone.kubevirt.io", + Resource: "virtualmachineclones", + }, +} diff --git a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go b/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go new file mode 100644 index 0000000..16698bb --- /dev/null +++ b/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2024 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 kubevirt + +import ( + "fmt" + "testing" + + "sigs.k8s.io/yaml" +) + +func TestKubevirtRulesToYAML(t *testing.T) { + b, err := yaml.Marshal(KubevirtRewriteRules) + if err != nil { + t.Fatalf("should marshal kubevirt rules without error: %v", err) + } + + fmt.Printf("%s\n", string(b)) +} diff --git a/images/kube-api-rewriter/pkg/labels/context_values.go b/images/kube-api-rewriter/pkg/labels/context_values.go new file mode 100644 index 0000000..55e27ef --- /dev/null +++ b/images/kube-api-rewriter/pkg/labels/context_values.go @@ -0,0 +1,104 @@ +/* +Copyright 2024 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 labels + +import ( + "context" + "strconv" +) + +func ContextWithCommon(ctx context.Context, name, resource, method, watch, toTargetAction, fromTargetAction string) context.Context { + ctx = context.WithValue(ctx, resourceKey{}, resource) + ctx = context.WithValue(ctx, methodKey{}, method) + ctx = context.WithValue(ctx, watchKey{}, watch) + ctx = context.WithValue(ctx, toTargetActionKey{}, toTargetAction) + ctx = context.WithValue(ctx, toTargetActionKey{}, fromTargetAction) + return context.WithValue(ctx, nameKey{}, name) +} + +func ContextWithDecision(ctx context.Context, decision string) context.Context { + return context.WithValue(ctx, decisionKey{}, decision) +} + +func ContextWithStatus(ctx context.Context, status int) context.Context { + return context.WithValue(ctx, statusKey{}, strconv.Itoa(status)) +} + +type nameKey struct{} +type resourceKey struct{} +type methodKey struct{} +type watchKey struct{} +type decisionKey struct{} +type toTargetActionKey struct{} +type fromTargetActionKey struct{} +type statusKey struct{} + +func NameFromContext(ctx context.Context) string { + if method, ok := ctx.Value(nameKey{}).(string); ok { + return method + } + return "" +} + +func ResourceFromContext(ctx context.Context) string { + if method, ok := ctx.Value(resourceKey{}).(string); ok { + return method + } + return "" +} + +func MethodFromContext(ctx context.Context) string { + if method, ok := ctx.Value(methodKey{}).(string); ok { + return method + } + return "" +} + +func WatchFromContext(ctx context.Context) string { + if value, ok := ctx.Value(watchKey{}).(string); ok { + return value + } + return "" +} + +func ToTargetActionFromContext(ctx context.Context) string { + if value, ok := ctx.Value(toTargetActionKey{}).(string); ok { + return value + } + return "" +} + +func FromTargetActionFromContext(ctx context.Context) string { + if value, ok := ctx.Value(fromTargetActionKey{}).(string); ok { + return value + } + return "" +} + +func DecisionFromContext(ctx context.Context) string { + if decision, ok := ctx.Value(decisionKey{}).(string); ok { + return decision + } + return "" +} + +func StatusFromContext(ctx context.Context) string { + if decision, ok := ctx.Value(statusKey{}).(string); ok { + return decision + } + return "" +} diff --git a/images/kube-api-rewriter/pkg/log/attrs.go b/images/kube-api-rewriter/pkg/log/attrs.go new file mode 100644 index 0000000..09c3ff0 --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/attrs.go @@ -0,0 +1,31 @@ +/* +Copyright 2024 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 log + +import "log/slog" + +func SlogErr(err error) slog.Attr { + return slog.Any("err", err) +} + +func BodyDiff(diff string) slog.Attr { + return slog.String(BodyDiffKey, diff) +} + +func BodyDump(dump string) slog.Attr { + return slog.String(BodyDumpKey, dump) +} diff --git a/images/kube-api-rewriter/pkg/log/body.go b/images/kube-api-rewriter/pkg/log/body.go new file mode 100644 index 0000000..6cf3d7d --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/body.go @@ -0,0 +1,83 @@ +/* +Copyright 2024 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 log + +import ( + "bytes" + "fmt" + "io" +) + +// ReaderLogger is ReadCloser implementation that catches content +// while underlying Reader is being read, e.g. with io.Copy. +// Content is copied into the buffer and may be used after copying +// for logging or other handling. +type ReaderLogger struct { + wrappedReader io.ReadCloser + buf bytes.Buffer +} + +func NewReaderLogger(r io.Reader) *ReaderLogger { + rdr := &ReaderLogger{} + rdr.wrappedReader = io.NopCloser(io.TeeReader(r, &rdr.buf)) + return rdr +} + +func (r *ReaderLogger) Read(p []byte) (n int, err error) { + return r.wrappedReader.Read(p) +} + +func (r *ReaderLogger) Close() error { + return r.wrappedReader.Close() +} + +func HeadString(obj interface{}, limit int) string { + readLog, ok := obj.(*ReaderLogger) + if !ok { + return "" + } + bufLen := readLog.buf.Len() + bufStr := readLog.buf.String() + if bufLen < limit { + return bufStr + } + return bufStr[0:limit] +} + +func HeadStringEx(obj interface{}, limit int) string { + s := HeadString(obj, limit) + if s == "" { + return "" + } + return fmt.Sprintf("[%d] %s", len(s), s) +} + +func HasData(obj interface{}) bool { + readLog, ok := obj.(*ReaderLogger) + if !ok { + return false + } + return readLog.buf.Len() > 0 +} + +func Bytes(obj interface{}) []byte { + readLog, ok := obj.(*ReaderLogger) + if !ok { + return nil + } + return readLog.buf.Bytes() +} diff --git a/images/kube-api-rewriter/pkg/log/differ.go b/images/kube-api-rewriter/pkg/log/differ.go new file mode 100644 index 0000000..e9a4c86 --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/differ.go @@ -0,0 +1,133 @@ +/* +Copyright 2024 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 log + +import ( + "bytes" + "fmt" + "log/slog" + + jd "github.com/josephburnett/jd/lib" + "github.com/tidwall/gjson" +) + +// DebugBodyChanges logs debug message with diff between 2 bodies. +func DebugBodyChanges(logger *slog.Logger, msg string, resourceType string, inBytes, rwrBytes []byte) { + if !logger.Enabled(nil, slog.LevelDebug) { + return + } + + // No changes were made to inBytes. + if rwrBytes == nil { + logger.Debug(fmt.Sprintf("%s: no changes after rewrite", msg)) + return + } + + if len(inBytes) == 0 && len(rwrBytes) == 0 { + logger.Debug(fmt.Sprintf("%s: empty body", msg)) + return + } + + if len(inBytes) == 0 && len(rwrBytes) != 0 { + logger.Debug(fmt.Sprintf("%s: possible bug: empty body produces %d bytes", msg, len(rwrBytes))) + DebugBodyHead(logger, msg, resourceType, rwrBytes) + return + } + + if len(inBytes) != 0 && len(rwrBytes) == 0 { + logger.Error(fmt.Sprintf("%s: possible bug: non-empty body [%d] produces empty rewrite", msg, len(inBytes))) + DebugBodyHead(logger, msg, resourceType, inBytes) + return + } + + // Print diff for non-empty non-equal JSONs. + diffContent, err := Diff(inBytes, rwrBytes) + if err != nil { + // Rollback to printing a limited part of the JSON. + logger.Error(fmt.Sprintf("Can't diff '%s' JSONs after rewrite", resourceType), SlogErr(err)) + DebugBodyHead(logger, msg, resourceType, rwrBytes) + return + } + + // TODO pass ns/name as arguments for patches. + apiVersion := gjson.GetBytes(inBytes, "apiVersion") + kind := gjson.GetBytes(inBytes, "kind") + ns := gjson.GetBytes(inBytes, "metadata.namespace") + name := gjson.GetBytes(inBytes, "metadata.name") + logger.Debug(fmt.Sprintf("%s: changes after rewrite for %s/%s/%s/%s", msg, ns, apiVersion, kind, name), BodyDiff(diffContent)) +} + +// DebugBodyHead logs head of input slice. +func DebugBodyHead(logger *slog.Logger, msg, resourceType string, obj []byte) { + limit := 1024 + switch resourceType { + case "virtualmachines", + "virtualmachines/status", + "virtualmachineinstances", + "virtualmachineinstances/status", + "clustervirtualimages", + "clustervirtualimages/status", + "clusterrolebindings", + "customresourcedefinitions": + limit = 32000 + } + if resourceType == "patch" { + limit = len(obj) + } + logger.Debug(fmt.Sprintf("%s: dump rewritten body", msg), BodyDump(headBytes(obj, limit))) +} + +func headBytes(msg []byte, limit int) string { + s := string(msg) + msgLen := len(s) + if msgLen == 0 { + return "" + } + // Lower the limit if message is shorter than the limit. + if msgLen < limit { + limit = msgLen + } + return fmt.Sprintf("[%d] %s", msgLen, s[0:limit]) +} + +// Diff returns a human-readable diff between 2 JSONs suitable for debugging. +// See: https://github.com/josephburnett/jd/blob/master/README.md +func Diff(json1, json2 []byte) (string, error) { + // Handle some edge cases. + switch { + case json1 == nil && json2 != nil: + return "", fmt.Errorf("got %d rewritten bytes without original", len(json2)) + case json1 != nil && json2 == nil: + return "", nil + case json1 == nil && json2 == nil: + return "", nil + case bytes.Equal(json1, json2): + return "", nil + } + + // Calculate diff between JSONs. + jd.Setkeys("name") + a, err := jd.ReadJsonString(string(json1)) + if err != nil { + return "", err + } + b, err := jd.ReadJsonString(string(json2)) + if err != nil { + return "", err + } + return a.Diff(b).Render(), nil +} diff --git a/images/kube-api-rewriter/pkg/log/pretty_handler.go b/images/kube-api-rewriter/pkg/log/pretty_handler.go new file mode 100644 index 0000000..39586fe --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/pretty_handler.go @@ -0,0 +1,248 @@ +/* +Copyright 2024 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 log + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "runtime" + "sort" + "sync" + + "github.com/kr/text" + "sigs.k8s.io/yaml" +) + +// PrettyHandler is a custom handler to print pretty debug logs: +// - Print attributes unquoted +// - Print body.dump and body.diff as sections +// +// Notes on implementation: record in the Handle method contains only attrs from Info/Debug calls, +// other Attrs are stored inside parent Handlers. There is no way to access those attributes +// in a simple manner, e.g. via slog exposed methods. +// Internal slog logic around Attrs includes grouping, preformatting, replacing. It is not simple +// to reimplement it, so lazy JsonHandler workaround is used to re-use this internal machinery +// in exchange to performance. This handler is meant to use for debugging purposes, so it is OK. +// +// For one who brave enough to optimize this Handler, please, please, read these sources thoroughly: +// - https://dusted.codes/creating-a-pretty-console-logger-using-gos-slog-package +// - https://betterstack.com/community/guides/logging/logging-in-go/ +// - https://github.com/golang/example/tree/master/slog-handler-guide + +const BodyDiffKey = "body.diff" +const BodyDumpKey = "body.dump" + +const dateTimeWithSecondsFrac = "2006-01-02 15:04:05.000" + +// PrettyHandler is a pretty print handler close to default slog handler. +type PrettyHandler struct { + jh slog.Handler + jhb *bytes.Buffer + jhmu *sync.Mutex + w io.Writer + wmu *sync.Mutex + opts *slog.HandlerOptions +} + +func NewPrettyHandler(w io.Writer, opts *slog.HandlerOptions) *PrettyHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + b := &bytes.Buffer{} + return &PrettyHandler{ + jh: slog.NewJSONHandler(b, &slog.HandlerOptions{ + Level: opts.Level, + AddSource: opts.AddSource, + ReplaceAttr: suppressDefaultAttrs(opts.ReplaceAttr), + }), + jhb: b, + jhmu: &sync.Mutex{}, + w: w, + wmu: &sync.Mutex{}, + opts: opts, + } +} + +// Enabled returns if level is enabled for this handler. +func (h *PrettyHandler) Enabled(ctx context.Context, l slog.Level) bool { + return h.jh.Enabled(ctx, l) +} + +func (h *PrettyHandler) WithAttrs(as []slog.Attr) slog.Handler { + return &PrettyHandler{ + jh: h.jh.WithAttrs(as), + jhb: h.jhb, + jhmu: h.jhmu, + w: h.w, + wmu: h.wmu, + opts: h.opts, + } +} + +// WithGroup adds group +func (h *PrettyHandler) WithGroup(name string) slog.Handler { + return &PrettyHandler{ + jh: h.jh.WithGroup(name), + jhb: h.jhb, + jhmu: h.jhmu, + w: h.w, + wmu: h.wmu, + opts: h.opts, + } +} + +func (h *PrettyHandler) Handle(ctx context.Context, r slog.Record) error { + // Get all attributes set by parent Handlers via JsonHandler. + allAttrs, err := h.gatherAttrs(ctx, r) + if err != nil { + return err + } + + // Separate dumps and other attributes. + dumps := make(map[string]string) + groups := make(map[string]any) + attrs := make([]slog.Attr, 0) + for attrKey, attr := range allAttrs { + switch v := attr.(type) { + case map[string]any, []any: + groups[attrKey] = v + case string: + switch attrKey { + case BodyDumpKey, BodyDiffKey: + dumps[attrKey] = v + default: + attrs = append(attrs, slog.String(attrKey, v)) + } + default: + attrs = append(attrs, slog.Any(attrKey, attr)) + } + } + + var b bytes.Buffer + // Write main line: time, level, message and attributes. + b.WriteString(r.Time.Format(dateTimeWithSecondsFrac)) + b.WriteString(" ") + + b.WriteString(r.Level.String()) + b.WriteString(" ") + + b.WriteString(r.Message) + b.WriteString(" ") + + sort.Slice(attrs, func(i, j int) bool { + return attrs[i].Key < attrs[j].Key + }) + for i, attr := range attrs { + if i > 0 { + b.WriteString(" ") + } + b.WriteString(attr.Key) + b.WriteString("=\"") + b.WriteString(attr.Value.String()) + b.WriteString("\"") + } + ensureEndingNewLine(&b) + + if h.opts != nil && h.opts.AddSource && r.PC != 0 { + fs := runtime.CallersFrames([]uintptr{r.PC}) + f, _ := fs.Next() + b.WriteString(fmt.Sprintf(" source=%s:%d %s\n", f.File, f.Line, f.Function)) + } + + // Add sectioned info: grouped attributes, a body diff and a body dump. + if len(groups) > 0 { + groupsBytes, err := yaml.Marshal(groups) + if err != nil { + return fmt.Errorf("error marshaling grouped attrs: %w", err) + } + //b.WriteString("Grouped attrs:\n") + b.Write(text.IndentBytes(groupsBytes, []byte(" "))) + ensureEndingNewLine(&b) + } + + for _, dumpName := range []string{BodyDumpKey, BodyDiffKey} { + if diff, ok := dumps[dumpName]; ok { + b.WriteString(fmt.Sprintf(" %s:\n", dumpName)) + b.WriteString(text.Indent(diff, " ")) + ensureEndingNewLine(&b) + } + } + + //if diff, ok := dumps[BodyDiffKey]; ok { + // b.WriteString(" body.diff:\n") + // b.WriteString(text.Indent(diff, " ")) + // ensureEndingNewLine(&b) + //} + // + //if dump, ok := dumps[BodyDumpKey]; ok { + // b.WriteString(" body.dump:\n") + // b.WriteString(text.Indent(dump, " ")) + // ensureEndingNewLine(&b) + //} + + // Use Mutex to sync access to the shared Writer. + h.wmu.Lock() + defer h.wmu.Unlock() + _, err = b.WriteTo(h.w) + return err +} + +func ensureEndingNewLine(buf *bytes.Buffer) { + last := string(buf.Bytes()[buf.Len()-1:]) + if last != "\n" { + buf.WriteString("\n") + } +} + +func (h *PrettyHandler) gatherAttrs(ctx context.Context, r slog.Record) (map[string]any, error) { + h.jhmu.Lock() + defer func() { + h.jhb.Reset() + h.jhmu.Unlock() + }() + if err := h.jh.Handle(ctx, r); err != nil { + return nil, fmt.Errorf("error when calling inner handler's Handle: %w", err) + } + + var attrs map[string]any + err := json.Unmarshal(h.jhb.Bytes(), &attrs) + if err != nil { + return nil, fmt.Errorf("error when unmarshaling inner handler's Handle result: %w", err) + } + return attrs, nil +} + +func suppressDefaultAttrs( + next func([]string, slog.Attr) slog.Attr, +) func([]string, slog.Attr) slog.Attr { + return func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.TimeKey || + a.Key == slog.LevelKey || + a.Key == slog.MessageKey || + a.Key == slog.SourceKey { + return slog.Attr{} + } + if next == nil { + return a + } + return next(groups, a) + } +} diff --git a/images/kube-api-rewriter/pkg/log/pretty_handler_test.go b/images/kube-api-rewriter/pkg/log/pretty_handler_test.go new file mode 100644 index 0000000..a856cb8 --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/pretty_handler_test.go @@ -0,0 +1,72 @@ +/* +Copyright 2024 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 log + +import ( + "log/slog" + "os" + "testing" +) + +func TestDefaultCustomHandler(t *testing.T) { + slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + AddSource: true, + //Level: nil, + //ReplaceAttr: nil, + }))) + + logg := slog.With( + slog.Group("properties", + slog.Int("width", 4000), + slog.Int("height", 3000), + slog.String("format", "jpeg"), + slog.Group("nestedprops", + slog.String("arg", "val"), + ), + ), + slog.String("azaz", "foo"), + ) + logg.Info("message with group", + slog.Group("properties", + slog.Int("width", 6000), + ), + ) + + // set PrettyHandler as default + //dbgHandler := NewPrettyHandler(os.Stdout, nil) + dbgHandler := NewPrettyHandler(os.Stdout, &slog.HandlerOptions{AddSource: true}) + + slog.SetDefault(slog.New(dbgHandler)) + + logger := slog.With( + slog.String("arg1", "val1"), + slog.String("body.diff", "+-+-+-+\n++--++--\n + qwe\n - azaz"), + slog.Group("properties", + slog.Int("width", 6000), + ), + ) + + logger.Info("info message") + + logger = slog.With( + slog.String("arg1", "val1"), + slog.String("body.diff", "+-+-+-+"), + ) + logger.WithGroup("properties").Info("info message", + slog.Int("width", 6000), + ) +} diff --git a/images/kube-api-rewriter/pkg/log/setup.go b/images/kube-api-rewriter/pkg/log/setup.go new file mode 100644 index 0000000..2b1beec --- /dev/null +++ b/images/kube-api-rewriter/pkg/log/setup.go @@ -0,0 +1,120 @@ +/* +Copyright 2024 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 log + +import ( + "io" + "log/slog" + "os" + "strings" +) + +type Format string + +const ( + JSONLog Format = "json" + TextLog Format = "text" + PrettyLog Format = "pretty" +) + +type Output string + +const ( + Stdout Output = "stdout" + Stderr Output = "stderr" + Discard Output = "discard" +) + +// Defaults +const ( + DefaultLogLevel = slog.LevelInfo + DefaultDebugLogFormat = PrettyLog + DefaultLogFormat = JSONLog +) + +var DefaultLogOutput = os.Stdout + +type Options struct { + Level string + Format string + Output string +} + +func SetupDefaultLoggerFromEnv(opts Options) { + handler := SetupHandler(opts) + if handler != nil { + slog.SetDefault(slog.New(handler)) + } +} + +func SetupHandler(opts Options) slog.Handler { + logLevel := detectLogLevel(opts.Level) + logOutput := detectLogOutput(opts.Output) + logFormat := detectLogFormat(opts.Format, logLevel) + + logHandlerOpts := &slog.HandlerOptions{Level: logLevel} + switch logFormat { + case TextLog: + return slog.NewTextHandler(logOutput, logHandlerOpts) + case JSONLog: + return slog.NewJSONHandler(logOutput, logHandlerOpts) + case PrettyLog: + return NewPrettyHandler(logOutput, logHandlerOpts) + } + return nil +} + +func detectLogLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "error": + return slog.LevelError + case "warn": + return slog.LevelWarn + case "info": + return slog.LevelInfo + case "debug": + return slog.LevelDebug + } + return DefaultLogLevel +} + +func detectLogFormat(format string, level slog.Level) Format { + switch strings.ToLower(format) { + case string(TextLog): + return TextLog + case string(JSONLog): + return JSONLog + case string(PrettyLog): + return PrettyLog + } + if level == slog.LevelDebug { + return DefaultDebugLogFormat + } + return DefaultLogFormat +} + +func detectLogOutput(output string) io.Writer { + switch strings.ToLower(output) { + case string(Stdout): + return os.Stdout + case string(Stderr): + return os.Stderr + case string(Discard): + return io.Discard + } + return DefaultLogOutput +} diff --git a/images/kube-api-rewriter/pkg/monitoring/healthz/handler.go b/images/kube-api-rewriter/pkg/monitoring/healthz/handler.go new file mode 100644 index 0000000..d523b23 --- /dev/null +++ b/images/kube-api-rewriter/pkg/monitoring/healthz/handler.go @@ -0,0 +1,35 @@ +/* +Copyright 2024 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 healthz + +import "net/http" + +// AddHealthzHandler adds endpoints for health and readiness probes. +func AddHealthzHandler(mux *http.ServeMux) { + if mux == nil { + return + } + mux.HandleFunc("/healthz", okStatusHandler) + mux.HandleFunc("/healthz/", okStatusHandler) + mux.HandleFunc("/readyz", okStatusHandler) + mux.HandleFunc("/readyz/", okStatusHandler) +} + +func okStatusHandler(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("ok")) +} diff --git a/images/kube-api-rewriter/pkg/monitoring/metrics/handler.go b/images/kube-api-rewriter/pkg/monitoring/metrics/handler.go new file mode 100644 index 0000000..522a964 --- /dev/null +++ b/images/kube-api-rewriter/pkg/monitoring/metrics/handler.go @@ -0,0 +1,34 @@ +/* +Copyright 2024 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 metrics + +import ( + "net/http" + + "github.com/prometheus/client_golang/prometheus/promhttp" +) + +func AddMetricsHandler(mux *http.ServeMux) { + if mux == nil { + return + } + + handler := promhttp.HandlerFor(Registry, promhttp.HandlerOpts{ + ErrorHandling: promhttp.HTTPErrorOnError, + }) + mux.Handle("/metrics", handler) +} diff --git a/images/kube-api-rewriter/pkg/monitoring/metrics/registry.go b/images/kube-api-rewriter/pkg/monitoring/metrics/registry.go new file mode 100644 index 0000000..363aa96 --- /dev/null +++ b/images/kube-api-rewriter/pkg/monitoring/metrics/registry.go @@ -0,0 +1,40 @@ +/* +Copyright 2024 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 metrics + +import ( + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" +) + +// RegistererGatherer combines both parts of the API of a Prometheus +// registry, both the Registerer and the Gatherer interfaces. +type RegistererGatherer interface { + prometheus.Registerer + prometheus.Gatherer +} + +// Registry is our instance of the prometheus registry for storing metrics. +var Registry RegistererGatherer = prometheus.NewRegistry() + +func Init() { + Registry.MustRegister( + collectors.NewBuildInfoCollector(), + collectors.NewGoCollector(), + collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}), + ) +} diff --git a/images/kube-api-rewriter/pkg/monitoring/profiler/handler.go b/images/kube-api-rewriter/pkg/monitoring/profiler/handler.go new file mode 100644 index 0000000..01d4335 --- /dev/null +++ b/images/kube-api-rewriter/pkg/monitoring/profiler/handler.go @@ -0,0 +1,35 @@ +/* +Copyright 2024 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 profiler + +import ( + "net/http" + "net/http/pprof" +) + +// NewPprofHandler returns http.ServeMux with pprof endpoints. +func NewPprofHandler() http.Handler { + mux := http.NewServeMux() + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + return mux +} diff --git a/images/kube-api-rewriter/pkg/proxy/bytes_counter.go b/images/kube-api-rewriter/pkg/proxy/bytes_counter.go new file mode 100644 index 0000000..a03ced3 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/bytes_counter.go @@ -0,0 +1,76 @@ +/* +Copyright 2024 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 proxy + +import ( + "io" + "sync/atomic" +) + +func BytesCounterReaderWrap(r io.Reader) io.ReadCloser { + return &bytesCounter{origReader: r} +} + +func BytesCounterWriterWrap(w io.Writer) io.Writer { + return &bytesCounter{origWriter: w} +} + +var _ io.ReadCloser = &bytesCounter{} +var _ io.Writer = &bytesCounter{} + +type bytesCounter struct { + origReader io.Reader + origWriter io.Writer + counter atomic.Int64 +} + +func (r *bytesCounter) Read(p []byte) (n int, err error) { + l, err := r.origReader.Read(p) + r.counter.Add(int64(l)) + return l, err +} + +func (r *bytesCounter) Write(p []byte) (n int, err error) { + l, err := r.origWriter.Write(p) + r.counter.Add(int64(l)) + return l, err +} + +func (r *bytesCounter) Close() error { + return nil +} + +func (r *bytesCounter) Reset() { + r.counter.Store(0) +} + +func (r *bytesCounter) Count() int { + return int(r.counter.Load()) +} + +func CounterReset(wrapped interface{}) { + if bytesCounter, ok := wrapped.(*bytesCounter); ok { + bytesCounter.Reset() + } +} + +func CounterValue(wrapped interface{}) int { + if bytesCounter, ok := wrapped.(*bytesCounter); ok { + return bytesCounter.Count() + } + return 0 +} diff --git a/images/kube-api-rewriter/pkg/proxy/doc.go b/images/kube-api-rewriter/pkg/proxy/doc.go new file mode 100644 index 0000000..f33937c --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/doc.go @@ -0,0 +1,55 @@ +/* +Copyright 2024 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 proxy + +// Proxy handler implements 2 types of proxy: +// - proxy for client interaction with Kubernetes API Server +// - proxy to deliver AdmissionReview requests from Kubernetes API Server to webhook server +// +// Proxy for webhooks acts as follows: +// ServerHTTP method reads request from Kubernetes API Server, restores apiVersion, kind and +// ownerRefs, sends it to real webhook, renames apiVersion, kind, and ownerRefs +// and sends it back to Kubernetes API Server. +// +// +--------------------------------------------+ +// | Kubernetes API Server | +// +--------------------------------------------+ +// | ^ +// | | +// 1. AdmissionReview request 4. AdmissionReview response +// webhook.srv:443/webhook-endpoint | +// apiVersion: renamed-group.io | +// kind: PrefixedResource | +// | | +// v | +// +-----------------------------------------------------+ +// | Proxy | +// | 2. Restore 3. Rename | +// | apiVersion, kind field if Admission response | +// | in Admission request has patchType: JSONPatch | +// | in Admission request rename kind in ownerRef | +// +-----------------------------------------------------+ +// | ^ +// 127.0.0.1:9443/webhook-endpoint | +// apiVersion: original-group.io | +// kind: Resource | +// | | +// v | +// +-------------------------------------------------------+ +// | Webhook | +// | handles request ---> sends response | +// +-------------------------------------------------------+ diff --git a/images/kube-api-rewriter/pkg/proxy/handler.go b/images/kube-api-rewriter/pkg/proxy/handler.go new file mode 100644 index 0000000..a9dcb12 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/handler.go @@ -0,0 +1,551 @@ +/* +Copyright 2024 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 proxy + +import ( + "bytes" + "compress/flate" + "compress/gzip" + "context" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "net/url" + "strconv" + "strings" + "sync" + "time" + + "github.com/tidwall/gjson" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/deckhouse/kube-api-rewriter/pkg/labels" + logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" +) + +type ProxyMode string + +const ( + // ToOriginal mode indicates that resource should be restored when passed to target and renamed when passing back to client. + ToOriginal ProxyMode = "original" + // ToRenamed mode indicates that resource should be renamed when passed to target and restored when passing back to client. + ToRenamed ProxyMode = "renamed" +) + +func ToTargetAction(proxyMode ProxyMode) rewriter.Action { + if proxyMode == ToRenamed { + return rewriter.Rename + } + return rewriter.Restore +} + +func FromTargetAction(proxyMode ProxyMode) rewriter.Action { + if proxyMode == ToRenamed { + return rewriter.Restore + } + return rewriter.Rename +} + +type Handler struct { + Name string + // ProxyPass is a target http client to send requests to. + // An allusion to nginx proxy_pass directive. + TargetClient *http.Client + TargetURL *url.URL + ProxyMode ProxyMode + Rewriter *rewriter.RuleBasedRewriter + MetricsProvider MetricsProvider + streamHandler *StreamHandler + m sync.Mutex +} + +func (h *Handler) Init() { + if h.MetricsProvider == nil { + h.MetricsProvider = NewMetricsProvider() + } + h.streamHandler = &StreamHandler{ + Rewriter: h.Rewriter, + MetricsProvider: h.MetricsProvider, + } +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if req == nil { + slog.Error("req is nil. something wrong") + return + } + if req.URL == nil { + slog.Error(fmt.Sprintf("req.URL is nil. something wrong. method %s RequestURI '%s' Headers %+v", req.Method, req.RequestURI, req.Header)) + return + } + + requestHandleStart := time.Now() + + // Step 1. Parse request url, prepare path rewrite. + targetReq := rewriter.NewTargetRequest(h.Rewriter, req) + + resource := targetReq.ResourceForLog() + toTargetAction := string(ToTargetAction(h.ProxyMode)) + fromTargetAction := string(FromTargetAction(h.ProxyMode)) + ctx := labels.ContextWithCommon(req.Context(), h.Name, resource, req.Method, WatchLabel(targetReq.IsWatch()), toTargetAction, fromTargetAction) + + logger := LoggerWithCommonAttrs(ctx, + slog.String("url.path", req.URL.Path), + ) + + metrics := NewProxyMetrics(ctx, h.MetricsProvider) + metrics.GotClientRequest() + + // Set target address, cleanup RequestURI. + req.RequestURI = "" + req.URL.Scheme = h.TargetURL.Scheme + req.URL.Host = h.TargetURL.Host + + // Log request path. + rwrReq := " NO" + if targetReq.ShouldRewriteRequest() { + rwrReq = "REQ" + } + rwrResp := " NO" + if targetReq.ShouldRewriteResponse() { + rwrResp = "RESP" + } + if targetReq.Path() != req.URL.Path || targetReq.RawQuery() != req.URL.RawQuery { + logger.Info(fmt.Sprintf("%s [%s,%s] %s -> %s", req.Method, rwrReq, rwrResp, req.URL.RequestURI(), targetReq.RequestURI())) + } else { + logger.Info(fmt.Sprintf("%s [%s,%s] %s", req.Method, rwrReq, rwrResp, req.URL.String())) + } + + // TODO(development): Mute some logging for development: election, non-rewritable resources. + isMute := false + if !targetReq.ShouldRewriteRequest() && !targetReq.ShouldRewriteResponse() { + isMute = true + } + switch resource { + case "leases": + isMute = true + case "endpoints": + isMute = true + case "clusterrolebindings": + isMute = false + case "clustervirtualmachineimages": + isMute = false + case "virtualmachines": + isMute = false + case "virtualmachines/status": + isMute = false + } + if isMute { + logger = slog.New(slog.NewTextHandler(io.Discard, nil)) + } + + logger.Debug(fmt.Sprintf("Request: orig headers: %+v", req.Header)) + + // Step 2. Modify request endpoint, headers and body bytes before send it to the target. + origRequestBytes, rwrRequestBytes, err := h.transformRequest(targetReq, req) + if err != nil { + logger.Error(fmt.Sprintf("Error transforming request: %s", req.URL.String()), logutil.SlogErr(err)) + http.Error(w, "can't rewrite request", http.StatusBadRequest) + metrics.ClientRequestRewriteError() + return + } + + logger.Debug(fmt.Sprintf("Request: target headers: %+v", req.Header)) + + // Restore req.Body as this reader was read earlier by the transformRequest. + clientBodyDecision := decisionPass + if rwrRequestBytes != nil { + req.Body = BytesCounterReaderWrap(bytes.NewBuffer(rwrRequestBytes)) + metrics.ClientRequestRewriteSuccess() + clientBodyDecision = decisionRewrite + // metrics.ClientRequestRewriteDuration() + } else if origRequestBytes != nil { + // Fallback to origRequestBytes if body was not rewritten. + req.Body = BytesCounterReaderWrap(bytes.NewBuffer(origRequestBytes)) + } + + metrics.FromClientBytesAdd(clientBodyDecision, len(origRequestBytes)) + + // Step 3. Send request to the target. + resp, err := h.TargetClient.Do(req) + if err != nil { + logger.Error("Error passing request to the target", logutil.SlogErr(err)) + http.Error(w, k8serrors.NewInternalError(err).Error(), http.StatusInternalServerError) + metrics.TargetResponseError() + return + } + + ctx = labels.ContextWithStatus(ctx, resp.StatusCode) + metrics = NewProxyMetrics(ctx, h.MetricsProvider) + metrics.ToTargetBytesAdd(clientBodyDecision, CounterValue(req.Body)) + metrics.TargetResponseSuccess(clientBodyDecision) + + // Save original Body to close when handler finishes. + origRespBody := resp.Body + defer func() { + origRespBody.Close() + }() + + // TODO handle resp.Status 3xx, 4xx, 5xx, etc. + + if req.Method == http.MethodPatch { + logutil.DebugBodyHead(logger, "Request PATCH", "patch", origRequestBytes) + logutil.DebugBodyChanges(logger, "Request PATCH", "patch", origRequestBytes, rwrRequestBytes) + } else { + logutil.DebugBodyChanges(logger, "Request", resource, origRequestBytes, rwrRequestBytes) + } + + // Step 5. Handle response: pass through, transform resp.Body, or run stream transformer. + + if !targetReq.ShouldRewriteResponse() { + ctx = labels.ContextWithDecision(ctx, decisionPass) + metrics = NewProxyMetrics(ctx, h.MetricsProvider) + // Pass response as-is without rewriting. + if targetReq.IsWatch() { + logger.Debug(fmt.Sprintf("Response decision: PASS STREAM, Status %s, Headers %+v", resp.Status, resp.Header)) + } else { + logger.Debug(fmt.Sprintf("Response decision: PASS, Status %s, Headers %+v", resp.Status, resp.Header)) + } + h.passResponse(ctx, targetReq, w, resp, logger) + metrics.RequestDuration(time.Since(requestHandleStart)) + return + } + + ctx = labels.ContextWithDecision(ctx, decisionRewrite) + metrics = NewProxyMetrics(ctx, h.MetricsProvider) + + if targetReq.IsWatch() { + logger.Debug(fmt.Sprintf("Response decision: REWRITE STREAM, Status %s, Headers %+v", resp.Status, resp.Header)) + + h.transformStream(ctx, targetReq, w, resp, logger) + metrics.RequestDuration(time.Since(requestHandleStart)) + return + } + + // One-time rewrite is required for client or webhook requests. + logger.Debug(fmt.Sprintf("Response decision: REWRITE, Status %s, Headers %+v", resp.Status, resp.Header)) + + h.transformResponse(ctx, targetReq, w, resp, logger) + metrics.RequestDuration(time.Since(requestHandleStart)) + return +} + +func copyHeader(dst, src http.Header) { + for header, values := range src { + // Do not override dst header with the header from the src. + if len(dst.Values(header)) > 0 { + continue + } + for _, value := range values { + dst.Add(header, value) + } + } +} + +// resp.Header.Get("Content-Encoding") +func encodingAwareReaderWrap(bodyReader io.ReadCloser, encoding string) (io.ReadCloser, error) { + var reader io.ReadCloser + var err error + + switch encoding { + case "gzip": + reader, err = gzip.NewReader(bodyReader) + if err != nil { + return nil, fmt.Errorf("errorf making gzip reader: %v", err) + } + return io.NopCloser(reader), nil + case "deflate": + return flate.NewReader(bodyReader), nil + } + + return bodyReader, nil +} + +// transformRequest transforms request headers and rewrites request payload to use +// request as client to the target. +// TargetMode field defines either transformer should rename resources +// if request is from the client, or restore resources if it is a call +// from the API Server to the webhook. +func (h *Handler) transformRequest(targetReq *rewriter.TargetRequest, req *http.Request) ([]byte, []byte, error) { + if req == nil || req.URL == nil { + return nil, nil, fmt.Errorf("http request and URL should not be nil") + } + + var origBodyBytes []byte + var rwrBodyBytes []byte + var err error + + hasPayload := req.Body != nil + + if hasPayload { + origBodyBytes, err = io.ReadAll(req.Body) + if err != nil { + return nil, nil, fmt.Errorf("read request body: %w", err) + } + } + + // Rewrite incoming payload, e.g. create, put, etc. + if targetReq.ShouldRewriteRequest() && hasPayload { + switch req.Method { + case http.MethodPatch: + rwrBodyBytes, err = h.Rewriter.RewritePatch(targetReq, origBodyBytes) + default: + rwrBodyBytes, err = h.Rewriter.RewriteJSONPayload(targetReq, origBodyBytes, ToTargetAction(h.ProxyMode)) + } + if err != nil { + return nil, nil, err + } + + // Put new Body reader to req and fix Content-Length header. + rwrBodyLen := len(rwrBodyBytes) + if rwrBodyLen > 0 { + // Fix content-length if needed. + req.ContentLength = int64(rwrBodyLen) + if req.Header.Get("Content-Length") != "" { + req.Header.Set("Content-Length", strconv.Itoa(rwrBodyLen)) + } + } + } + + // TODO Implement protobuf and table rewriting to remove these manipulations with Accept header. + // TODO Move out to a separate method forceApplicationJSONContent. + if targetReq.ShouldRewriteResponse() { + newAccept := make([]string, 0) + for _, hdr := range req.Header.Values("Accept") { + // Rewriter doesn't work with protobuf, force JSON in Accept header. + // This workaround is suitable only for empty body requests: Get, List, etc. + // A client should be patched to send JSON requests. + if strings.Contains(hdr, "application/vnd.kubernetes.protobuf") { + newAccept = append(newAccept, "application/json") + continue + } + + // TODO Add rewriting support for Table format. + // Quickly support kubectl with simple hack + if strings.Contains(hdr, "application/json") && strings.Contains(hdr, "as=Table") { + newAccept = append(newAccept, "application/json") + continue + } + + newAccept = append(newAccept, hdr) + } + + req.Header["Accept"] = newAccept + + // Force JSON for watches of core resources and CRDs. + if targetReq.IsWatch() && (targetReq.IsCRD() || targetReq.IsCore()) { + if len(req.Header.Values("Accept")) == 0 { + req.Header["Accept"] = []string{"application/json"} + } + } + } + + // Set new endpoint path and query. + req.URL.Path = targetReq.Path() + req.URL.RawQuery = targetReq.RawQuery() + + return origBodyBytes, rwrBodyBytes, nil +} + +func (h *Handler) passResponse(ctx context.Context, targetReq *rewriter.TargetRequest, w http.ResponseWriter, resp *http.Response, logger *slog.Logger) { + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) + + bodyReader := resp.Body + + dst := &immediateWriter{dst: w} + + if logger.Enabled(nil, slog.LevelDebug) { + if targetReq.IsWatch() { + dst.chunkFn = func(chunk []byte) { + logger.Debug(fmt.Sprintf("Pass through response chunk: %s", string(chunk))) + } + } else { + bodyReader = logutil.NewReaderLogger(bodyReader) + } + } + + metrics := NewProxyMetrics(ctx, h.MetricsProvider) + + // Wrap body reader with bytes counter to set to_client_bytes metric. + bytesCounterBody := BytesCounterReaderWrap(bodyReader) + + _, err := io.Copy(dst, bytesCounterBody) + if err != nil { + logger.Error(fmt.Sprintf("copy target response back to client: %v", err)) + metrics.RequestHandleError() + } else { + metrics.ToClientBytesAdd(CounterValue(bytesCounterBody)) + metrics.RequestHandleSuccess() + } + + if logger.Enabled(nil, slog.LevelDebug) && !targetReq.IsWatch() { + logutil.DebugBodyHead(logger, + fmt.Sprintf("Pass through response: status %d, content-length: '%s'", resp.StatusCode, resp.Header.Get("Content-Length")), + targetReq.ResourceForLog(), + logutil.Bytes(bodyReader), + ) + } + + return +} + +// transformResponse rewrites payloads in responses from the target. +// +// ProxyMode field defines either rewriter should restore, or rename resources. +func (h *Handler) transformResponse(ctx context.Context, targetReq *rewriter.TargetRequest, w http.ResponseWriter, resp *http.Response, logger *slog.Logger) { + metrics := NewProxyMetrics(ctx, h.MetricsProvider) + + var err error + bytesCounter := BytesCounterReaderWrap(resp.Body) + // Add gzip decoder if needed. + bodyReader, err := encodingAwareReaderWrap(bytesCounter, resp.Header.Get("Content-Encoding")) + if err != nil { + logger.Error("Error decoding response body", logutil.SlogErr(err)) + http.Error(w, "can't decode response body", http.StatusInternalServerError) + metrics.RequestHandleError() + return + } + // Close needed for gzip and flate readers. + defer bodyReader.Close() + + // Step 1. Read response body to buffer. + origBodyBytes, err := io.ReadAll(bodyReader) + if err != nil { + logger.Error("Error reading response payload", logutil.SlogErr(err)) + http.Error(w, "Error reading response payload", http.StatusBadGateway) + metrics.RequestHandleError() + return + } + + metrics.FromTargetBytesAdd(CounterValue(bytesCounter)) + + // Rewrite supports only json responses for now. Pass invalid JSON and non-JSON responses as-is. + if !gjson.ValidBytes(origBodyBytes) { + contentType := resp.Header.Get("Content-Type") + if strings.HasPrefix(contentType, "application/json") { + logger.Warn(fmt.Sprintf("Will not transform invalid JSON response from target: Content-type=%s", contentType)) + } else { + logger.Warn(fmt.Sprintf("Will not transform non JSON response from target: Content-type=%s", contentType)) + } + + metrics.TargetResponseInvalidJSON(resp.StatusCode) + + h.passResponse(ctx, targetReq, w, resp, logger) + return + } + + // Step 2. Rewrite response JSON. + rewriteStart := time.Now() + statusCode := resp.StatusCode + rwrBodyBytes, err := h.Rewriter.RewriteJSONPayload(targetReq, origBodyBytes, FromTargetAction(h.ProxyMode)) + if err != nil { + if !errors.Is(err, rewriter.SkipItem) { + logger.Error("Error rewriting response", logutil.SlogErr(err)) + http.Error(w, "can't rewrite response", http.StatusInternalServerError) + metrics.RequestHandleError() + metrics.TargetResponseRewriteError() + return + } + // Return NotFound Status object if rewriter decides to skip resource. + rwrBodyBytes = notFoundJSON(targetReq.OrigResourceType(), origBodyBytes) + statusCode = http.StatusNotFound + } + metrics.TargetResponseRewriteSuccess() + metrics.TargetResponseRewriteDuration(time.Since(rewriteStart)) + + if targetReq.IsWebhook() { + logutil.DebugBodyHead(logger, "Response from webhook", targetReq.ResourceForLog(), origBodyBytes) + } + logutil.DebugBodyChanges(logger, "Response", targetReq.ResourceForLog(), origBodyBytes, rwrBodyBytes) + + // Step 3. Fix headers before sending response back to the client. + copyHeader(w.Header(), resp.Header) + // Fix Content headers. + // rwrBodyBytes are always decoded from gzip. Delete header to not break our client. + w.Header().Del("Content-Encoding") + if rwrBodyBytes != nil { + w.Header().Set("Content-Length", strconv.Itoa(len(rwrBodyBytes))) + } + w.WriteHeader(statusCode) + + // Step 4. Write non-empty rewritten body to the client. + if rwrBodyBytes != nil { + copied, err := w.Write(rwrBodyBytes) + if err != nil { + logger.Error(fmt.Sprintf("error writing response from target to the client: %v", err)) + metrics.RequestHandleError() + } else { + metrics.RequestHandleSuccess() + metrics.ToClientBytesAdd(copied) + } + } + + return +} + +func (h *Handler) transformStream(ctx context.Context, targetReq *rewriter.TargetRequest, w http.ResponseWriter, resp *http.Response, logger *slog.Logger) { + // Rewrite body as a stream. ServeHTTP will block until context cancel. + err := h.streamHandler.Handle(ctx, w, resp, targetReq) + if err != nil { + logger.Error("Error watching stream", logutil.SlogErr(err)) + http.Error(w, fmt.Sprintf("watch stream: %v", err), http.StatusInternalServerError) + } +} + +type immediateWriter struct { + dst io.Writer + chunkFn func([]byte) +} + +func (iw *immediateWriter) Write(p []byte) (n int, err error) { + n, err = iw.dst.Write(p) + + if iw.chunkFn != nil { + iw.chunkFn(p) + } + + if flusher, ok := iw.dst.(http.Flusher); ok { + flusher.Flush() + } + + return +} + +// notFoundJSON constructs Status response of type NotFound +// for resourceType and object name. +// Example: +// +// { +// "kind":"Status", +// "apiVersion":"v1", +// "metadata":{}, +// "status":"Failure", +// "message":"pods \"vmi-router-x9mqwdqwd\" not found", +// "reason":"NotFound", +// "details":{"name":"vmi-router-x9mqwdqwd","kind":"pods"}, +// "code":404} +func notFoundJSON(resourceType string, obj []byte) []byte { + objName := gjson.GetBytes(obj, "metadata.name").String() + details := fmt.Sprintf(`"details":{"name":"%s","kind":"%s"}`, objName, resourceType) + message := fmt.Sprintf(`"message":"%s %s not found"`, resourceType, objName) + notFoundTpl := `{"kind":"Status","apiVersion":"v1",%s,%s,"metadata":{},"status":"Failure","reason":"NotFound","code":404}` + return []byte(fmt.Sprintf(notFoundTpl, message, details)) +} diff --git a/images/kube-api-rewriter/pkg/proxy/handler_test.go b/images/kube-api-rewriter/pkg/proxy/handler_test.go new file mode 100644 index 0000000..265d4d5 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/handler_test.go @@ -0,0 +1,778 @@ +/* +Copyright 2024 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 proxy + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/pprof" + "net/url" + "runtime" + "sort" + "strconv" + "strings" + "sync" + "testing" + "time" + + "github.com/tidwall/gjson" + + "github.com/deckhouse/kube-api-rewriter/pkg/kubevirt" + "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" + "github.com/deckhouse/kube-api-rewriter/pkg/server" +) + +// PodJSON is a real Pod example to test JSON rewrites. +const PodJSON = `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "example-pod", + "annotations": { + "cni.cilium.io/ipAddress": "10.66.10.1", + "kubectl.kubernetes.io/default-container": "compute", + "kubevirt.internal.virtualization.deckhouse.io/allow-pod-bridge-network-live-migration": "true", + "kubevirt.internal.virtualization.deckhouse.io/domain": "cloud-alpine", + "kubevirt.internal.virtualization.deckhouse.io/migrationTransportUnix": "true", + "kubevirt.internal.virtualization.deckhouse.io/vm-generation": "1", + "post.hook.backup.velero.io/command": "[\"/usr/bin/virt-freezer\", \"--unfreeze\", \"--name\", \"cloud-alpine\", \"--namespace\", \"vm\"]", + "post.hook.backup.velero.io/container": "compute", + "pre.hook.backup.velero.io/command": "[\"/usr/bin/virt-freezer\", \"--freeze\", \"--name\", \"cloud-alpine\", \"--namespace\", \"vm\"]", + "pre.hook.backup.velero.io/container": "compute" + }, + "creationTimestamp": "2024-10-01T11:45:59Z", + "finalizers": [ + "virtualization.deckhouse.io/pod-protection" + ], + "generateName": "virt-launcher-cloud-alpine-", + "labels": { + "kubevirt.internal.virtualization.deckhouse.io": "virt-launcher", + "kubevirt.internal.virtualization.deckhouse.io/created-by": "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f", + "kubevirt.internal.virtualization.deckhouse.io/nodeName": "virtlab-delivery-mi-2", + "vm": "cloud-alpine", + "vm-folder": "vm-cloud-alpine", + "vm.kubevirt.internal.virtualization.deckhouse.io/name": "cloud-alpine" + }, + "name": "virt-launcher-cloud-alpine-lxlz5", + "namespace": "vm", + "ownerReferences": [ + { + "apiVersion": "internal.virtualization.deckhouse.io/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "InternalVirtualizationVirtualMachineInstance", + "name": "cloud-alpine", + "uid": "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f" + } + ], + "resourceVersion": "595346645", + "uid": "68558c6e-aefb-4cbb-922a-e8389e8ce43f" + }, + "spec": { + "affinity": { + "nodeAffinity": { + "requiredDuringSchedulingIgnoredDuringExecution": { + "nodeSelectorTerms": [ + { + "matchExpressions": [ + { + "key": "node-role.kubernetes.io/control-plane", + "operator": "DoesNotExist" + } + ] + } + ] + } + } + }, + "automountServiceAccountToken": false, + "containers": [ + { + "command": [ + "/usr/bin/virt-launcher-monitor", + "--qemu-timeout", + "338s", + "--name", + "cloud-alpine", + "--uid", + "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f", + "--namespace", + "vm", + "--kubevirt-share-dir", + "/var/run/kubevirt", + "--ephemeral-disk-dir", + "/var/run/kubevirt-ephemeral-disks", + "--container-disk-dir", + "/var/run/kubevirt/container-disks", + "--grace-period-seconds", + "75", + "--hook-sidecars", + "0", + "--ovmf-path", + "/usr/share/OVMF" + ], + "env": [ + { + "name": "POD_NAME", + "valueFrom": { + "fieldRef": { + "apiVersion": "v1", + "fieldPath": "metadata.name" + } + } + } + ], + "image": "dev-registry.deckhouse.io/sys/deckhouse-oss/modules/virtualization@sha256:c3c6c6a87ce0082697da80a6e53b4bf59fb433be05cabd9f7c46201bd45283e6", + "imagePullPolicy": "IfNotPresent", + "name": "compute", + "resources": { + "limits": { + "cpu": "4", + "devices.virtualization.deckhouse.io/kvm": "1", + "devices.virtualization.deckhouse.io/tun": "1", + "devices.virtualization.deckhouse.io/vhost-net": "1", + "memory": "4582277121" + }, + "requests": { + "cpu": "4", + "devices.virtualization.deckhouse.io/kvm": "1", + "devices.virtualization.deckhouse.io/tun": "1", + "devices.virtualization.deckhouse.io/vhost-net": "1", + "ephemeral-storage": "50M", + "memory": "4582277121" + } + }, + "securityContext": { + "capabilities": { + "add": [ + "NET_BIND_SERVICE", + "SYS_NICE" + ] + }, + "privileged": false, + "runAsNonRoot": false, + "runAsUser": 0 + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeDevices": [ + { + "devicePath": "/dev/vd-cloud-alpine", + "name": "vd-cloud-alpine" + }, + { + "devicePath": "/dev/vd-cloud-alpine-data", + "name": "vd-cloud-alpine-data" + } + ], + "volumeMounts": [ + { + "mountPath": "/var/run/kubevirt-private", + "name": "private" + }, + { + "mountPath": "/var/run/kubevirt", + "name": "public" + }, + { + "mountPath": "/var/run/kubevirt-ephemeral-disks", + "name": "ephemeral-disks" + }, + { + "mountPath": "/var/run/kubevirt/container-disks", + "mountPropagation": "HostToContainer", + "name": "container-disks" + }, + { + "mountPath": "/var/run/libvirt", + "name": "libvirt-runtime" + }, + { + "mountPath": "/var/run/kubevirt/sockets", + "name": "sockets" + }, + { + "mountPath": "/var/run/kubevirt/hotplug-disks", + "mountPropagation": "HostToContainer", + "name": "hotplug-disks" + } + ] + } + ], + + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": false, + "hostname": "cloud-alpine", + "nodeName": "virtlab-delivery-mi-2", + "nodeSelector": { + "cpu-model.node.virtualization.deckhouse.io/Nehalem": "true", + "kubernetes.io/arch": "amd64", + "kubevirt.internal.virtualization.deckhouse.io/schedulable": "true" + }, + "preemptionPolicy": "PreemptLowerPriority", + "priority": 1000, + "priorityClassName": "develop", + "readinessGates": [ + { + "conditionType": "kubevirt.io/virtual-machine-unpaused" + } + ], + "restartPolicy": "Never", + "schedulerName": "linstor", + "securityContext": { + "runAsUser": 0 + }, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 90, + + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoSchedule", + "key": "node.kubernetes.io/memory-pressure", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "devices.virtualization.deckhouse.io/kvm", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "devices.virtualization.deckhouse.io/tun", + "operator": "Exists" + }, + { + "effect": "NoSchedule", + "key": "devices.virtualization.deckhouse.io/vhost-net", + "operator": "Exists" + } + ], + + "volumes": [ + { + "emptyDir": {}, + "name": "private" + }, + { + "emptyDir": {}, + "name": "public" + }, + { + "emptyDir": {}, + "name": "sockets" + }, + { + "emptyDir": {}, + "name": "virt-bin-share-dir" + }, + { + "emptyDir": {}, + "name": "libvirt-runtime" + }, + { + "emptyDir": {}, + "name": "ephemeral-disks" + }, + { + "emptyDir": {}, + "name": "container-disks" + }, + { + "name": "vd-cloud-alpine", + "persistentVolumeClaim": { + "claimName": "vd-cloud-alpine-30e0ce5d-d0d7-4f38-b0a2-493330e5bb4a" + } + }, + { + "name": "vd-cloud-alpine-data", + "persistentVolumeClaim": { + "claimName": "vd-cloud-alpine-data-23941f64-7241-40a1-8fc1-f976c7c364e8" + } + }, + { + "emptyDir": {}, + "name": "hotplug-disks" + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": null, + "status": "False", + "type": "Custom" + }, + { + "lastProbeTime": "2024-10-01T11:45:59Z", + "lastTransitionTime": "2024-10-01T11:45:59Z", + "message": "the virtual machine is not paused", + "reason": "NotPaused", + "status": "True", + "type": "kubevirt.io/virtual-machine-unpaused" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-10-01T11:45:59Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-10-01T11:46:01Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-10-01T11:46:01Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2024-10-01T11:45:59Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "containerd://4305d5ef79c16cbb9f28450506f9ec4650269e8034bdd0d5d42189aa638effb4", + "image": "sha256:cf321ffda57daa4fbf19daf047506fd36a841ced39ef869a80cc53a6387bba26", + "imageID": "dev-registry.deckhouse.io/sys/deckhouse-oss/modules/virtualization@sha256:c3c6c6a87ce0082697da80a6e53b4bf59fb433be05cabd9f7c46201bd45283e6", + "lastState": {}, + "name": "compute", + "ready": true, + "restartCount": 0, + "started": true, + "state": { + "running": { + "startedAt": "2024-10-01T11:46:01Z" + } + } + } + ], + "hostIP": "172.18.18.72", + "phase": "Running", + "podIP": "10.66.10.1", + "podIPs": [ + { + "ip": "10.66.10.1" + } + ], + "qosClass": "Guaranteed", + "startTime": "2024-10-01T11:45:59Z" + } +}` + +// Test_run_proxy_with_pprof runs server, rewriter and a client +// in different go routines for experimenting with pprof. +// +// Start test and run go tool: +// +// go tool pprof -http=127.0.0.1:8085 http://127.0.0.1:43200/debug/pprof/heap +func Test_run_proxy_with_pprof(t *testing.T) { + // Comment to run experiments. + t.SkipNow() + + // Memory stats printer. + go func() { + ticker := time.NewTicker(3 * time.Second) + for { + <-ticker.C + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + fmt.Printf( + "Heap Alloc: %0.2f MB, Heap InUse %0.2f MB\n", + float64(stats.HeapAlloc)/1024/1024, + float64(stats.HeapInuse)/1024/1024, + ) + } + }() + + // Pprof server + go func() { + mux := http.NewServeMux() + + mux.HandleFunc("/debug/pprof/", pprof.Index) + mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) + mux.HandleFunc("/debug/pprof/profile", pprof.Profile) + mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) + mux.HandleFunc("/debug/pprof/trace", pprof.Trace) + + pprofSrv := &http.Server{ + Addr: "127.0.0.1:43200", + Handler: mux, + } + + fmt.Println("Pprof server started at 127.0.0.1:43200") + if err := pprofSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Println("Error starting pprof server:", err) + } + }() + + // This HTTP server implements List Pods endpoint of the Kubernetes API Server. + kubeAPIRready := make(chan struct{}, 0) + // Change count to stress test the rewriter. + podsCount := 3200 + go func() { + items := strings.Repeat(PodJSON+",", podsCount-1) + PodsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` + + once := 0 + + handleGet := func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(PodsListJSON))) + w.WriteHeader(http.StatusOK) + wrbytes, err := io.Copy(w, bytes.NewBuffer([]byte(PodsListJSON))) + if err != nil { + t.Fatalf("Should send pod list: %v", err) + } + + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + if once == 0 { + fmt.Printf("pods requested, send %d bytes (%d written)\n", len(PodsListJSON), wrbytes) + once = 1 + } + } + + handleRequest := func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + handleGet(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/namespaces/vm/pods", handleRequest) + + kubeAPISrv := &http.Server{ + Addr: "127.0.0.1:43215", + Handler: mux, + } + + fmt.Println("Server started at 127.0.0.1:43215") + close(kubeAPIRready) + if err := kubeAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Println("Error starting server:", err) + } + }() + + // This HTTP server runs the rewriter. Switch client to use it to detect problems with proxy handler. + go func() { + items := strings.Repeat(PodJSON+",", podsCount-1) + PodsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` + + rewriteRules := kubevirt.KubevirtRewriteRules + rewriteRules.Init() + + rwr := &rewriter.RuleBasedRewriter{ + Rules: rewriteRules, + } + + once := 0 + + handleGet := func(w http.ResponseWriter, r *http.Request) { + rwrBytes, err := rwr.RewriteJSONPayload(nil, []byte(PodsListJSON), rewriter.Rename) + if err != nil { + t.Fatalf("Should rewrite JSON pod list: %v", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(rwrBytes))) + w.WriteHeader(http.StatusOK) + wrbytes, err := io.Copy(w, bytes.NewBuffer(rwrBytes)) + if err != nil { + t.Fatalf("Should send pod list: %v", err) + } + + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + + if once == 0 { + fmt.Printf("pods requested, send %d bytes (%d written)\n", len(PodsListJSON), wrbytes) + once = 1 + } + } + + handleRequest := func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + handleGet(w, r) + default: + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + } + } + + mux := http.NewServeMux() + mux.HandleFunc("/api/v1/namespaces/vm/pods", handleRequest) + + kubeAPISrv := &http.Server{ + Addr: "127.0.0.1:43217", + Handler: mux, + } + + fmt.Println("Server started at 127.0.0.1:43217") + if err := kubeAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + fmt.Println("Error starting server:", err) + } + }() + + // A rewriter proxy. + go func() { + log.SetupDefaultLoggerFromEnv(log.Options{ + Level: "debug", + Format: "pretty", + Output: "discard", + }) + //slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) + + apiServerURL := "http://127.0.0.1:43215" + targetURL, err := url.Parse(apiServerURL) + if err != nil { + t.Fatalf("Should parse url %s: %v", apiServerURL, err) + return + } + + rewriteRules := kubevirt.KubevirtRewriteRules + rewriteRules.Init() + + rwr := &rewriter.RuleBasedRewriter{ + Rules: rewriteRules, + } + proxyHandler := &Handler{ + Name: "test-mem-leak", + TargetClient: &http.Client{}, + TargetURL: targetURL, + ProxyMode: ToRenamed, + Rewriter: rwr, + } + + srv := &server.HTTPServer{ + InstanceDesc: "Test Mem Leak", + ListenAddr: "127.0.0.1:43216", + RootHandler: proxyHandler, + } + + srv.Start() + }() + + <-kubeAPIRready + + fmt.Println("Start spamming ...") + + // Spam proxy with requests. + start := time.Now() + spamDuration := time.Minute + sleepDuration := time.Minute + //maxCount := 2200000 + count := 1 + for { + // Choose what source to test. + // No proxy, no rewrites. + // req, err := http.NewRequest("GET", "http://127.0.0.1:43215/api/v1/namespaces/vm/pods", nil) + // No proxy, only rewriter. + req, err := http.NewRequest("GET", "http://127.0.0.1:43217/api/v1/namespaces/vm/pods", nil) + // Proxy and rewriter. + // req, err := http.NewRequest("GET", "http://127.0.0.1:43216/api/v1/namespaces/vm/pods", nil) + if err != nil { + t.Fatalf("Should not fail on creating request %d: %v", count, err) + return + } + + startRequest := time.Now() + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + t.Fatalf("Should not fail on GET request %d: %v", count, err) + return + } + + startRead := time.Now() + + podBytes, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatalf("Should not fail on reading response %d: %v", count, err) + return + } + endRead := time.Now() + + resp.Body.Close() + + respKind := gjson.GetBytes(podBytes, "kind").String() + if respKind != "PodList" { + t.Fatalf("Got unexpected kind: %s", respKind) + return + } + + endKind := time.Now() + + if count == 1 { + dur := endRead.Sub(startRequest) + speed := float64(len(podBytes)) / dur.Seconds() / 1024 + fmt.Printf("Request time: %s, Speed: %0.2f kb/s\n", startRead.Sub(startRequest).Truncate(time.Millisecond).String(), speed) + fmt.Printf("Read time: %s, Speed: %0.2f kb/s\n", endRead.Sub(startRead).Truncate(time.Millisecond).String(), speed) + fmt.Printf("Whole time: %s\n", endKind.Sub(startRequest).Truncate(time.Millisecond).String()) + fmt.Printf("%d. Got %s. Read %d bytes.\n", count, respKind, len(podBytes)) + } + + now := time.Now() + if now.Sub(start) > spamDuration { + fmt.Printf("Send %d requests in %s\n", count, now.Sub(start).Truncate(time.Second).String()) + break + } + + podBytes = nil + + count++ + //if count == maxCount { + // return + //} + } + + time.Sleep(sleepDuration) +} + +// Test_RewriteJSONPayload_time runs RewriteJSONPayload +// with different PodList lengths and outputs time stats. +// +// Example: +// +// === RUN Test_RewriteJSONPayload_time +// Got 9 results +// 100 expect: 1.78s got: 1.78s x1.00 +// 200 expect: 3.56s got: 1.875s x0.53 +// 400 expect: 7.12s got: 2.39s x0.34 +// 800 expect: 14.24s got: 3.83s x0.27 +// 1600 expect: 28.48s got: 4.709s x0.17 +// 3200 expect: 56.96s got: 6.077s x0.11 +// 6400 expect: 1m53.921s got: 8.396s x0.07 +// 12800 expect: 3m47.842s got: 12.013s x0.05 +// 25600 expect: 7m35.685s got: 17.271s x0.04 +func Test_RewriteJSONPayload_time(t *testing.T) { + t.SkipNow() + + rewriteRules := kubevirt.KubevirtRewriteRules + rewriteRules.Init() + + rwr := &rewriter.RuleBasedRewriter{ + Rules: rewriteRules, + } + + podListCounts := []int{ + 100, + 200, + 400, + 800, + 1600, + 3200, + 6400, + 12800, + 25600, + } + + var wg sync.WaitGroup + wg.Add(len(podListCounts)) + + type testRes struct { + count int + execDur time.Duration + bytesCount int + rwrBytesCount int + } + + resCh := make(chan testRes, len(podListCounts)) + + for _, podListCount := range podListCounts { + go func(podsCount int) { + // Construct PodList with podsCount items. Name uniqueness + // is not significant for the test purposes. + items := strings.Repeat(PodJSON+",", podsCount-1) + podsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` + + start := time.Now() + rwrBytes, err := rwr.RewriteJSONPayload(nil, []byte(podsListJSON), rewriter.Restore) + if err != nil { + t.Fatalf("Should rewrite JSON: %v", err) + return + } + end := time.Now() + + resCh <- testRes{ + count: podsCount, + execDur: end.Sub(start), + bytesCount: len(podsListJSON), + rwrBytesCount: len(rwrBytes), + } + + wg.Done() + }(podListCount) + } + + wg.Wait() + + // Extract results from the chan. + testResults := make([]testRes, 0, len(podListCounts)) + for range podListCounts { + res := <-resCh + testResults = append(testResults, res) + } + + // Print sorted results. + fmt.Printf("Got %d results\n", len(testResults)) + sort.SliceStable(testResults, func(i, j int) bool { + return testResults[i].count < testResults[j].count + }) + first := testResults[0] + for _, res := range testResults { + expectedDur := time.Duration(res.count/first.count) * first.execDur + ratio := float64(res.execDur) / float64(expectedDur) + + fmt.Printf("%5d expect: %10s got: %10s x%0.2f\n", + res.count, + expectedDur.Truncate(time.Millisecond).String(), + res.execDur.Truncate(time.Millisecond).String(), + ratio, + ) + } +} diff --git a/images/kube-api-rewriter/pkg/proxy/logger.go b/images/kube-api-rewriter/pkg/proxy/logger.go new file mode 100644 index 0000000..f6f2022 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/logger.go @@ -0,0 +1,35 @@ +/* +Copyright 2024 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 proxy + +import ( + "context" + "log/slog" + + "github.com/deckhouse/kube-api-rewriter/pkg/labels" +) + +func LoggerWithCommonAttrs(ctx context.Context, attrs ...any) *slog.Logger { + logger := slog.Default() + logger = logger.With( + slog.String("proxy.name", labels.NameFromContext(ctx)), + slog.String("resource", labels.ResourceFromContext(ctx)), + slog.String("method", labels.MethodFromContext(ctx)), + slog.String("watch", labels.WatchFromContext(ctx)), + ) + return logger.With(attrs...) +} diff --git a/images/kube-api-rewriter/pkg/proxy/metrics.go b/images/kube-api-rewriter/pkg/proxy/metrics.go new file mode 100644 index 0000000..90ca49c --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/metrics.go @@ -0,0 +1,126 @@ +/* +Copyright 2024 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 proxy + +import ( + "context" + "strconv" + "time" + + "github.com/deckhouse/kube-api-rewriter/pkg/labels" +) + +type ProxyMetrics struct { + provider MetricsProvider + name string + resource string + method string + watch string + decision string + side string + toTargetAction string + fromTargetAction string + status string +} + +func NewProxyMetrics(ctx context.Context, provider MetricsProvider) *ProxyMetrics { + return &ProxyMetrics{ + provider: provider, + name: labels.NameFromContext(ctx), + resource: labels.ResourceFromContext(ctx), + method: labels.MethodFromContext(ctx), + watch: labels.WatchFromContext(ctx), + decision: labels.DecisionFromContext(ctx), + toTargetAction: labels.ToTargetActionFromContext(ctx), + fromTargetAction: labels.FromTargetActionFromContext(ctx), + status: labels.StatusFromContext(ctx), + } +} + +func WatchLabel(isWatch bool) string { + if isWatch { + return watchRequest + } + return regularRequest +} + +func (p *ProxyMetrics) GotClientRequest() { + p.provider.NewClientRequestsTotal(p.name, p.resource, p.method, p.watch, p.decision).Inc() +} + +func (p *ProxyMetrics) TargetResponseSuccess(decision string) { + p.provider.NewTargetResponsesTotal(p.name, p.resource, p.method, p.watch, decision, p.status, noError).Inc() +} + +func (p *ProxyMetrics) TargetResponseError() { + p.provider.NewTargetResponsesTotal(p.name, p.resource, p.method, p.watch, "", p.status, errorOccurred).Inc() +} + +func (p *ProxyMetrics) TargetResponseInvalidJSON(status int) { + p.provider.NewTargetResponseInvalidJSONTotal(p.name, p.resource, p.method, p.watch, strconv.Itoa(status)) +} + +func (p *ProxyMetrics) RequestHandleSuccess() { + p.provider.NewRequestsHandledTotal(p.name, p.resource, p.method, p.watch, p.decision, p.status, noError).Inc() +} +func (p *ProxyMetrics) RequestHandleError() { + p.provider.NewRequestsHandledTotal(p.name, p.resource, p.method, p.watch, p.decision, p.status, errorOccurred).Inc() +} + +func (p *ProxyMetrics) RequestDuration(dur time.Duration) { + p.provider.NewRequestsHandlingSeconds(p.name, p.resource, p.method, p.watch, p.decision, p.status).Observe(dur.Seconds()) +} + +func (p *ProxyMetrics) TargetResponseRewriteError() { + p.provider.NewRewritesTotal(p.name, p.resource, p.method, p.watch, targetSide, p.fromTargetAction, errorOccurred).Inc() +} + +func (p *ProxyMetrics) TargetResponseRewriteSuccess() { + p.provider.NewRewritesTotal(p.name, p.resource, p.method, p.watch, targetSide, p.fromTargetAction, noError).Inc() +} + +func (p *ProxyMetrics) ClientRequestRewriteError() { + p.provider.NewRewritesTotal(p.name, p.resource, p.method, p.watch, clientSide, p.toTargetAction, errorOccurred).Inc() +} + +func (p *ProxyMetrics) ClientRequestRewriteSuccess() { + p.provider.NewRewritesTotal(p.name, p.resource, p.method, p.watch, clientSide, p.toTargetAction, noError).Inc() +} + +func (p *ProxyMetrics) ClientRequestRewriteDuration(dur time.Duration) { + p.provider.NewRewritesDurationSeconds(p.name, p.resource, p.method, p.watch, clientSide, p.toTargetAction).Observe(dur.Seconds()) +} + +func (p *ProxyMetrics) TargetResponseRewriteDuration(dur time.Duration) { + p.provider.NewRewritesDurationSeconds(p.name, p.resource, p.method, p.watch, targetSide, p.fromTargetAction).Observe(dur.Seconds()) +} + +func (p *ProxyMetrics) FromClientBytesAdd(decision string, count int) { + p.provider.NewFromClientBytesTotal(p.name, p.resource, p.method, p.watch, decision).Add(float64(count)) +} + +func (p *ProxyMetrics) ToTargetBytesAdd(decision string, count int) { + p.provider.NewToTargetBytesTotal(p.name, p.resource, p.method, p.watch, decision).Add(float64(count)) +} + +func (p *ProxyMetrics) FromTargetBytesAdd(count int) { + p.provider.NewFromTargetBytesTotal(p.name, p.resource, p.method, p.watch, p.decision).Add(float64(count)) +} + +func (p *ProxyMetrics) ToClientBytesAdd(count int) { + p.provider.NewToClientBytesTotal(p.name, p.resource, p.method, p.watch, p.decision).Add(float64(count)) +} diff --git a/images/kube-api-rewriter/pkg/proxy/metrics_provider.go b/images/kube-api-rewriter/pkg/proxy/metrics_provider.go new file mode 100644 index 0000000..8d48573 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/metrics_provider.go @@ -0,0 +1,276 @@ +/* +Copyright 2024 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 proxy + +import ( + "github.com/prometheus/client_golang/prometheus" + + "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/metrics" +) + +var Subsystem = defaultSubsystem + +const ( + defaultSubsystem = "kube_api_rewriter" + + clientRequestsTotalName = "client_requests_total" + targetResponsesTotalName = "target_responses_total" + targetResponseInvalidJSONTotalName = "target_response_invalid_json_total" + + requestsHandledTotalName = "requests_handled_total" + requestHandlingDurationSecondsName = "request_handling_duration_seconds" + + rewritesTotalName = "rewrites_total" + rewriteDurationSecondsName = "rewrite_duration_seconds" + + fromClientBytesName = "from_client_bytes_total" + toTargetBytesName = "to_target_bytes_total" + fromTargetBytesName = "from_target_bytes_total" + toClientBytesName = "to_client_bytes_total" + + nameLabel = "name" + resourceLabel = "resource" + methodLabel = "method" + watchLabel = "watch" + decisionLabel = "decision" + sideLabel = "side" + operationLabel = "operation" + statusLabel = "status" + errorLabel = "error" + + watchRequest = "1" + regularRequest = "0" + + decisionRewrite = "rewrite" + decisionPass = "pass" + + targetSide = "target" + clientSide = "client" + + operationRename = "rename" + operationRestore = "restore" + + errorOccurred = "1" + noError = "0" +) + +var ( + clientRequestsTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: clientRequestsTotalName, + Help: "Total number of received client requests", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel}) + + targetResponsesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: targetResponsesTotalName, + Help: "Total number of responses from the target", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel, statusLabel, errorLabel}) + + targetResponseInvalidJSONTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: targetResponseInvalidJSONTotalName, + Help: "Total target responses with invalid JSON", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, statusLabel}) + + requestsHandledTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: requestsHandledTotalName, + Help: "Total number of requests handled by the proxy instance", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel, statusLabel, errorLabel}) + + requestHandlingDurationSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Subsystem: Subsystem, + Name: requestHandlingDurationSecondsName, + Help: "Duration of request handling for non-watching and watch event handling for watch requests", + Buckets: []float64{ + 0.0, + 0.001, 0.002, 0.005, // 1, 2, 5 milliseconds + 0.01, 0.02, 0.05, // 10, 20, 50 milliseconds + 0.1, 0.2, 0.5, // 100, 200, 500 milliseconds + 1, 2, 5, // 1, 2, 5 seconds + }, + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel, statusLabel}) + + rewritesTotal = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: rewritesTotalName, + Help: "Total rewrites executed by the proxy instance", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, sideLabel, operationLabel, errorLabel}) + + rewritesDurationSeconds = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Subsystem: Subsystem, + Name: rewriteDurationSecondsName, + Help: "Duration of rewrite operations", + Buckets: []float64{ + 0.0, + 0.001, 0.002, 0.005, // 1, 2, 5 milliseconds + 0.01, 0.02, 0.05, // 10, 20, 50 milliseconds + 0.1, 0.2, 0.5, // 100, 200, 500 milliseconds + 1, 2, 5, // 1, 2, 5 seconds + }, + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, sideLabel, operationLabel}) + + fromClientBytes = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: fromClientBytesName, + Help: "Total bytes received from the client", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel}) + + toTargetBytes = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: toTargetBytesName, + Help: "Total bytes transferred to the target", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel}) + + fromTargetBytes = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: fromTargetBytesName, + Help: "Total bytes received from the target", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel}) + + toClientBytes = prometheus.NewCounterVec(prometheus.CounterOpts{ + Subsystem: Subsystem, + Name: toClientBytesName, + Help: "Total bytes transferred back to the client", + }, []string{nameLabel, resourceLabel, methodLabel, watchLabel, decisionLabel}) +) + +func RegisterMetrics() { + metrics.Registry.MustRegister( + clientRequestsTotal, + targetResponsesTotal, + targetResponseInvalidJSONTotal, + requestsHandledTotal, + requestHandlingDurationSeconds, + fromClientBytes, + toTargetBytes, + fromTargetBytes, + toClientBytes, + rewritesTotal, + rewritesDurationSeconds, + ) +} + +type MetricsProvider interface { + NewClientRequestsTotal(name, resource, method, watch, decision string) prometheus.Counter + NewTargetResponsesTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter + NewTargetResponseInvalidJSONTotal(name, resource, method, watch, status string) prometheus.Counter + NewRequestsHandledTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter + NewRequestsHandlingSeconds(name, resource, method, watch, decision, status string) prometheus.Observer + NewRewritesTotal(name, resource, method, watch, side, operation, error string) prometheus.Counter + NewRewritesDurationSeconds(name, resource, method, watch, side, operation string) prometheus.Observer + NewFromClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter + NewToTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter + NewFromTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter + NewToClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter +} + +func NewMetricsProvider() MetricsProvider { + return &proxyMetricsProvider{} +} + +type proxyMetricsProvider struct{} + +func (p *proxyMetricsProvider) NewClientRequestsTotal(name, resource, method, watch, decision string) prometheus.Counter { + return clientRequestsTotal.WithLabelValues(name, resource, method, watch, decision) +} + +func (p *proxyMetricsProvider) NewTargetResponsesTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter { + return targetResponsesTotal.WithLabelValues(name, resource, method, watch, decision, status, error) +} + +func (p *proxyMetricsProvider) NewTargetResponseInvalidJSONTotal(name, resource, method, watch, status string) prometheus.Counter { + return targetResponseInvalidJSONTotal.WithLabelValues(name, resource, method, watch, status) +} + +func (p *proxyMetricsProvider) NewRequestsHandledTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter { + return requestsHandledTotal.WithLabelValues(name, resource, method, watch, decision, status, error) +} + +func (p *proxyMetricsProvider) NewRequestsHandlingSeconds(name, resource, method, watch, decision, status string) prometheus.Observer { + return requestHandlingDurationSeconds.WithLabelValues(name, resource, method, watch, decision, status) +} + +func (p *proxyMetricsProvider) NewRewritesTotal(name, resource, method, watch, side, operation, error string) prometheus.Counter { + return rewritesTotal.WithLabelValues(name, resource, method, watch, side, operation, error) +} + +func (p *proxyMetricsProvider) NewRewritesDurationSeconds(name, resource, method, watch, side, operation string) prometheus.Observer { + return rewritesDurationSeconds.WithLabelValues(name, resource, method, watch, side, operation) +} + +func (p *proxyMetricsProvider) NewFromClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return fromClientBytes.WithLabelValues(name, resource, method, watch, decision) +} + +func (p *proxyMetricsProvider) NewToTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return toTargetBytes.WithLabelValues(name, resource, method, watch, decision) +} + +func (p *proxyMetricsProvider) NewFromTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return fromTargetBytes.WithLabelValues(name, resource, method, watch, decision) +} + +func (p *proxyMetricsProvider) NewToClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return toClientBytes.WithLabelValues(name, resource, method, watch, decision) +} + +func NoopMetricsProvider() MetricsProvider { + return noopMetricsProvider{} +} + +type noopMetric struct { + prometheus.Counter + prometheus.Observer +} + +type noopMetricsProvider struct{} + +func (_ noopMetricsProvider) NewClientRequestsTotal(name, resource, method, watch, decision string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewTargetResponsesTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewTargetResponseInvalidJSONTotal(name, resource, method, watch, status string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewRequestsHandledTotal(name, resource, method, watch, decision, status, error string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewRequestsHandlingSeconds(name, resource, method, watch, decision, status string) prometheus.Observer { + return noopMetric{} +} +func (_ noopMetricsProvider) NewRewritesTotal(name, resource, method, watch, side, operation, error string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewRewritesDurationSeconds(name, resource, method, watch, side, operation string) prometheus.Observer { + return noopMetric{} +} +func (_ noopMetricsProvider) NewFromClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewToTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewFromTargetBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return noopMetric{} +} +func (_ noopMetricsProvider) NewToClientBytesTotal(name, resource, method, watch, decision string) prometheus.Counter { + return noopMetric{} +} diff --git a/images/kube-api-rewriter/pkg/proxy/stream_handler.go b/images/kube-api-rewriter/pkg/proxy/stream_handler.go new file mode 100644 index 0000000..7d44dc3 --- /dev/null +++ b/images/kube-api-rewriter/pkg/proxy/stream_handler.go @@ -0,0 +1,311 @@ +/* +Copyright 2024 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 proxy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "mime" + "net/http" + "time" + + "github.com/tidwall/gjson" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer/streaming" + apiutilnet "k8s.io/apimachinery/pkg/util/net" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/kubernetes/scheme" + + logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" +) + +// StreamHandler reads a stream from the target, transforms events +// and sends them to the client. +type StreamHandler struct { + Rewriter *rewriter.RuleBasedRewriter + MetricsProvider MetricsProvider +} + +// streamRewriter reads a stream from the src reader, transforms events +// and sends them to the dst writer. +type streamRewriter struct { + dst io.Writer + bytesCounter io.ReadCloser + src io.ReadCloser + rewriter *rewriter.RuleBasedRewriter + targetReq *rewriter.TargetRequest + decoder streaming.Decoder + done chan struct{} + log *slog.Logger + metrics *ProxyMetrics +} + +// Handle starts a go routine to pass rewritten Watch Events +// from server to client. +// Sources: +// k8s.io/apimachinery@v0.26.1/pkg/watch/streamwatcher.go:100 receive method +// k8s.io/kubernetes@v1.13.0/staging/src/k8s.io/client-go/rest/request.go:537 wrapperFn, create framer. +// k8s.io/kubernetes@v1.13.0/staging/src/k8s.io/client-go/rest/request.go:598 instantiate watch NewDecoder +func (s *StreamHandler) Handle(ctx context.Context, w http.ResponseWriter, resp *http.Response, targetReq *rewriter.TargetRequest) error { + rewriterInstance := &streamRewriter{ + dst: w, + targetReq: targetReq, + rewriter: s.Rewriter, + done: make(chan struct{}), + log: LoggerWithCommonAttrs(ctx), + metrics: NewProxyMetrics(ctx, s.MetricsProvider), + } + err := rewriterInstance.init(resp) + if err != nil { + return err + } + + rewriterInstance.copyHeaders(w, resp) + + // Start rewriting stream. + go rewriterInstance.start(ctx) + + <-rewriterInstance.DoneChan() + return nil +} + +func (s *streamRewriter) init(resp *http.Response) (err error) { + s.bytesCounter = BytesCounterReaderWrap(resp.Body) + s.src = s.bytesCounter + + if s.log.Enabled(nil, slog.LevelDebug) { + s.src = logutil.NewReaderLogger(s.bytesCounter) + } + + contentType := resp.Header.Get("Content-Type") + s.decoder, err = createWatchDecoder(s.src, contentType) + return err +} + +func (s *streamRewriter) copyHeaders(w http.ResponseWriter, resp *http.Response) { + copyHeader(w.Header(), resp.Header) + w.WriteHeader(resp.StatusCode) +} + +// proxy reads result from the decoder in a loop, rewrites and writes to a client. +// Sources +// k8s.io/apimachinery@v0.26.1/pkg/watch/streamwatcher.go:100 receive method +func (s *streamRewriter) start(ctx context.Context) { + defer utilruntime.HandleCrash() + defer s.Stop() + + for { + // Read event from the server. + var got metav1.WatchEvent + s.log.Debug("Start decode from stream") + res, _, err := s.decoder.Decode(nil, &got) + s.metrics.FromTargetBytesAdd(CounterValue(s.bytesCounter)) + if s.log.Enabled(ctx, slog.LevelDebug) { + s.log.Debug("Got decoded WatchEvent from stream: %d bytes received", CounterValue(s.bytesCounter)) + } + CounterReset(s.bytesCounter) + + // Check if context was canceled. + select { + case <-ctx.Done(): + s.log.Debug("Context canceled, stop stream rewriter") + return + default: + } + + if err != nil { + switch err { + case io.EOF: + // Watch closed normally. + s.log.Debug("Catch EOF from target, stop proxying the stream") + case io.ErrUnexpectedEOF: + s.log.Error("Unexpected EOF during watch stream event decoding", logutil.SlogErr(err)) + default: + if apiutilnet.IsProbableEOF(err) || apiutilnet.IsTimeout(err) { + s.log.Error("Unable to decode an event from the watch stream", logutil.SlogErr(err)) + } else { + s.log.Error("Unable to decode an event from the watch stream", logutil.SlogErr(err)) + } + } + return + } + + watchEventHandleStart := time.Now() + + var rwrEvent *metav1.WatchEvent + if res != &got { + s.log.Warn(fmt.Sprintf("unable to decode to metav1.Event: res=%#v, got=%#v", res, got)) + s.metrics.TargetResponseInvalidJSON(200) + s.metrics.RequestHandleError() + // There is nothing to send to the client: no event decoded. + } else { + rwrEvent, err = s.transformWatchEvent(&got) + if err != nil && errors.Is(err, rewriter.SkipItem) { + s.log.Warn(fmt.Sprintf("Watch event '%s': skipped by rewriter", got.Type), logutil.SlogErr(err)) + logutil.DebugBodyHead(s.log, fmt.Sprintf("Watch event '%s' skipped", got.Type), s.targetReq.ResourceForLog(), got.Object.Raw) + s.metrics.RequestHandleSuccess() + } else { + if err != nil { + s.log.Error(fmt.Sprintf("Watch event '%s': transform error", got.Type), logutil.SlogErr(err)) + logutil.DebugBodyHead(s.log, fmt.Sprintf("Watch event '%s'", got.Type), s.targetReq.ResourceForLog(), got.Object.Raw) + } + if rwrEvent == nil { + // No rewrite, pass original event as-is. + rwrEvent = &got + } else { + // Log changes after rewrite. + logutil.DebugBodyChanges(s.log, "Watch event", s.targetReq.ResourceForLog(), got.Object.Raw, rwrEvent.Object.Raw) + } + // Pass event to the client. + logutil.DebugBodyHead(s.log, fmt.Sprintf("WatchEvent type '%s' send back to client %d bytes", rwrEvent.Type, len(rwrEvent.Object.Raw)), s.targetReq.ResourceForLog(), rwrEvent.Object.Raw) + s.writeEvent(rwrEvent) + } + } + + s.metrics.RequestDuration(time.Since(watchEventHandleStart)) + + // Check if application is stopped before waiting for the next event. + select { + case <-s.done: + return + default: + } + } +} + +func (s *streamRewriter) Stop() { + select { + case <-s.done: + default: + close(s.done) + } +} + +func (s *streamRewriter) DoneChan() chan struct{} { + return s.done +} + +// createSerializers +// Source +// k8s.io/client-go@v0.26.1/rest/request.go:765 newStreamWatcher +// k8s.io/apimachinery@v0.26.1/pkg/runtime/negotiate.go:70 StreamDecoder +func createWatchDecoder(r io.Reader, contentType string) (streaming.Decoder, error) { + mediaType, _, err := mime.ParseMediaType(contentType) + if err != nil { + return nil, fmt.Errorf("unexpected media type from the server: %q: %w", contentType, err) + } + + negotiatedSerializer := scheme.Codecs.WithoutConversion() + mediaTypes := negotiatedSerializer.SupportedMediaTypes() + info, ok := runtime.SerializerInfoForMediaType(mediaTypes, mediaType) + if !ok { + if len(contentType) != 0 || len(mediaTypes) == 0 { + return nil, fmt.Errorf("no matching serializer for media type '%s'", contentType) + } + info = mediaTypes[0] + } + if info.StreamSerializer == nil { + return nil, fmt.Errorf("no serializer for content type %s", contentType) + } + + // A chain of the framer and the serializer will split body stream into JSON objects. + frameReader := info.StreamSerializer.Framer.NewFrameReader(io.NopCloser(r)) + streamingDecoder := streaming.NewDecoder(frameReader, info.StreamSerializer.Serializer) + return streamingDecoder, nil +} + +func (s *streamRewriter) transformWatchEvent(ev *metav1.WatchEvent) (*metav1.WatchEvent, error) { + switch ev.Type { + case string(watch.Added), string(watch.Modified), string(watch.Deleted), string(watch.Error), string(watch.Bookmark): + default: + return nil, fmt.Errorf("got unknown type in WatchEvent: %v", ev.Type) + } + + group := gjson.GetBytes(ev.Object.Raw, "apiVersion").String() + kind := gjson.GetBytes(ev.Object.Raw, "kind").String() + name := gjson.GetBytes(ev.Object.Raw, "metadata.name").String() + ns := gjson.GetBytes(ev.Object.Raw, "metadata.namespace").String() + + // TODO add pass-as-is for non rewritable objects. + if group == "" && kind == "" { + // Object in event is undetectable, pass this event as-is. + return nil, fmt.Errorf("object has no apiVersion and kind") + } + s.log.Debug(fmt.Sprintf("Receive '%s' watch event with %s/%s %s/%s object", ev.Type, group, kind, ns, name)) + + var rwrObjBytes []byte + var err error + rewriteStart := time.Now() + defer func() { + s.metrics.TargetResponseRewriteDuration(time.Since(rewriteStart)) + }() + + if ev.Type == string(watch.Bookmark) { + // Temporarily print original BOOKMARK WatchEvent. + logutil.DebugBodyHead(s.log, fmt.Sprintf("Watch event '%s' from target", ev.Type), s.targetReq.OrigResourceType(), ev.Object.Raw) + rwrObjBytes, err = s.rewriter.RestoreBookmark(s.targetReq, ev.Object.Raw) + } else { + // Restore object in the event. Watch responses are always from the Kubernetes API server, so rename is not needed. + rwrObjBytes, err = s.rewriter.RewriteJSONPayload(s.targetReq, ev.Object.Raw, rewriter.Restore) + } + if err != nil { + if errors.Is(err, rewriter.SkipItem) { + s.metrics.TargetResponseRewriteSuccess() + return nil, err + } + s.metrics.TargetResponseRewriteError() + return nil, fmt.Errorf("rewrite object in WatchEvent '%s': %w", ev.Type, err) + } + + s.metrics.TargetResponseRewriteSuccess() + // Prepare rewritten event bytes. + return &metav1.WatchEvent{ + Type: ev.Type, + Object: runtime.RawExtension{ + Raw: rwrObjBytes, + }, + }, nil +} + +func (s *streamRewriter) writeEvent(ev *metav1.WatchEvent) { + rwrEventBytes, err := json.Marshal(ev) + if err != nil { + s.log.Error("encode restored event to bytes", logutil.SlogErr(err)) + return + } + + // Send rewritten event to the client. + copied, err := s.dst.Write(rwrEventBytes) + if err != nil { + s.log.Error("Watch event: error writing event to the client", logutil.SlogErr(err)) + s.metrics.RequestHandleSuccess() + s.metrics.ToClientBytesAdd(copied) + } else { + s.metrics.RequestHandleError() + } + // Flush writer to immediately send any buffered content to the client. + if wr, ok := s.dst.(http.Flusher); ok { + wr.Flush() + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/3rdparty.go b/images/kube-api-rewriter/pkg/rewriter/3rdparty.go new file mode 100644 index 0000000..915d73e --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/3rdparty.go @@ -0,0 +1,32 @@ +/* +Copyright 2024 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 rewriter + +// Rewrite routines for 3rd party resources, i.e. ServiceMonitor. + +const ( + PrometheusRuleKind = "PrometheusRule" + PrometheusRuleListKind = "PrometheusRuleList" + ServiceMonitorKind = "ServiceMonitor" + ServiceMonitorListKind = "ServiceMonitorList" +) + +func RewriteServiceMonitorOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return TransformObject(obj, "spec.selector", func(obj []byte) ([]byte, error) { + return rewriteLabelSelector(rules, obj, action) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/admission_configuration.go b/images/kube-api-rewriter/pkg/rewriter/admission_configuration.go new file mode 100644 index 0000000..6f881c8 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/admission_configuration.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 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 rewriter + +import "github.com/tidwall/gjson" + +const ( + ValidatingWebhookConfigurationKind = "ValidatingWebhookConfiguration" + ValidatingWebhookConfigurationListKind = "ValidatingWebhookConfigurationList" + MutatingWebhookConfigurationKind = "MutatingWebhookConfiguration" + MutatingWebhookConfigurationListKind = "MutatingWebhookConfigurationList" +) + +func RewriteValidatingOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, ValidatingWebhookConfigurationListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "webhooks", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + }) + } + return RewriteResourceOrList(obj, ValidatingWebhookConfigurationListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "webhooks", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) + }) +} + +func RewriteMutatingOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, MutatingWebhookConfigurationListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "webhooks", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + }) + } + return RewriteResourceOrList(obj, MutatingWebhookConfigurationListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "webhooks", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) + }) +} + +func RenameWebhookConfigurationPatch(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RenameMetadataPatch(rules, obj) + if err != nil { + return nil, err + } + + return TransformPatch(obj, func(mergePatch []byte) ([]byte, error) { + return RewriteArray(mergePatch, "webhooks", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) + }, func(jsonPatch []byte) ([]byte, error) { + path := gjson.GetBytes(jsonPatch, "path").String() + if path == "/webhooks" { + return RewriteArray(jsonPatch, "value", func(webhook []byte) ([]byte, error) { + return RewriteArray(webhook, "rules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + } + return jsonPatch, nil + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/admission_configuration_test.go b/images/kube-api-rewriter/pkg/rewriter/admission_configuration_test.go new file mode 100644 index 0000000..42c7f63 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/admission_configuration_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2024 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 rewriter + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestValidatingRename(t *testing.T) { + tests := []struct { + name string + manifest string + expect string + }{ + { + "mixed resources", + `{"webhooks":[{"rules":[{"apiGroups":[""],"resources":["pods"]},{"apiGroups": ["original.group.io"], "resources": ["someresources"]}]}]}`, + `{"webhooks":[{"rules":[{"apiGroups":[""],"resources":["pods"]},{"apiGroups": ["prefixed.resources.group.io"], "resources": ["prefixedsomeresources"]}]}]}`, + }, + { + "empty object", + `{}`, + `{}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rwr := createTestRewriter() + + resBytes, err := RewriteValidatingOrList(rwr.Rules, []byte(tt.manifest), Rename) + require.NoError(t, err, "should rename validating webhook configuration") + + actual := string(resBytes) + require.Equal(t, tt.expect, actual) + }) + } +} + +func TestValidatingRestore(t *testing.T) { + tests := []struct { + name string + manifest string + expect string + }{ + { + "mixed resources", + `{"webhooks":[{"rules":[{"apiGroups":[""],"resources":["pods"]},{"apiGroups": ["prefixed.resources.group.io"], "resources": ["prefixedsomeresources"]}]}]}`, + `{"webhooks":[{"rules":[{"apiGroups":[""],"resources":["pods"]},{"apiGroups": ["original.group.io"], "resources": ["someresources"]}]}]}`, + }, + { + "empty object", + `{}`, + `{}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rwr := createTestRewriter() + + resBytes, err := RewriteValidatingOrList(rwr.Rules, []byte(tt.manifest), Restore) + require.NoError(t, err, "should rename validating webhook configuration") + + actual := string(resBytes) + require.Equal(t, tt.expect, actual) + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/admission_policy.go b/images/kube-api-rewriter/pkg/rewriter/admission_policy.go new file mode 100644 index 0000000..f2f7265 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/admission_policy.go @@ -0,0 +1,67 @@ +/* +Copyright 2024 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 rewriter + +const ( + ValidatingAdmissionPolicyKind = "ValidatingAdmissionPolicy" + ValidatingAdmissionPolicyListKind = "ValidatingAdmissionPolicyList" + ValidatingAdmissionPolicyBindingKind = "ValidatingAdmissionPolicyBinding" + ValidatingAdmissionPolicyBindingListKind = "ValidatingAdmissionPolicyBindingList" +) + +// renames apiGroups and resources in a single resourceRule. +// Rule examples: +// resourceRules: +// - apiGroups: +// - "" +// apiVersions: +// - '*' +// operations: +// - '*' +// resources: +// - nodes +// scope: '*' + +func RewriteValidatingAdmissionPolicyOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, ValidatingAdmissionPolicyListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "spec.matchConstraints.resourceRules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + } + return RewriteResourceOrList(obj, ValidatingAdmissionPolicyListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "spec.matchConstraints.resourceRules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) +} + +func RewriteValidatingAdmissionPolicyBindingOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, ValidatingAdmissionPolicyBindingListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "spec.matchResources.resourceRules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + } + return RewriteResourceOrList(obj, ValidatingAdmissionPolicyBindingListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "spec.matchResources.resourceRules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/admission_review.go b/images/kube-api-rewriter/pkg/rewriter/admission_review.go new file mode 100644 index 0000000..613e3d9 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/admission_review.go @@ -0,0 +1,238 @@ +/* +Copyright 2024 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 rewriter + +import ( + "encoding/base64" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// RewriteAdmissionReview rewrites AdmissionReview request and response. +// NOTE: only one rewrite direction is supported for now: +// - Restore object in AdmissionReview request. +// - Do nothing for AdmissionReview response. +func RewriteAdmissionReview(rules *RewriteRules, obj []byte) ([]byte, error) { + if gjson.GetBytes(obj, "response").Exists() { + return TransformObject(obj, "response", func(responseObj []byte) ([]byte, error) { + return RenameAdmissionReviewResponse(rules, responseObj) + }) + } + + request := gjson.GetBytes(obj, "request") + if request.Exists() { + newRequest, err := RestoreAdmissionReviewRequest(rules, []byte(request.Raw)) + if err != nil { + return nil, err + } + if len(newRequest) > 0 { + obj, err = sjson.SetRawBytes(obj, "request", newRequest) + if err != nil { + return nil, err + } + } + } + + return obj, nil +} + +// RenameAdmissionReviewResponse renames metadata in AdmissionReview response patch. +// AdmissionReview response example: +// +// "response": { +// "uid": "", +// "allowed": true, +// "patchType": "JSONPatch", +// "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0=" +// } +// +// TODO rename annotations in AuditAnnotations field. (Ignore for now, as not used by the kubevirt). +func RenameAdmissionReviewResponse(rules *RewriteRules, obj []byte) ([]byte, error) { + // Description for the AdmissionResponse.PatchType field: The type of Patch. Currently, we only allow "JSONPatch". + patchType := gjson.GetBytes(obj, "patchType").String() + if patchType != "JSONPatch" { + return obj, nil + } + + // Get decoded patch. + b64Patch := gjson.GetBytes(obj, "patch").String() + if b64Patch == "" { + return obj, nil + } + + patch, err := base64.StdEncoding.DecodeString(b64Patch) + if err != nil { + return nil, fmt.Errorf("decode base64 patch: %w", err) + } + + rwrPatch, err := RenameMetadataPatch(rules, patch) + if err != nil { + return nil, fmt.Errorf("rename metadata patch: %w", err) + } + + // Update patch field to base64 encoded rewritten patch. + return sjson.SetBytes(obj, "patch", base64.StdEncoding.EncodeToString(rwrPatch)) +} + +// RestoreAdmissionReviewRequest restores apiVersion, kind and other fields in an AdmissionReview request. +// Only restoring is required, as AdmissionReview request only comes from API Server. +// Fields for AdmissionReview request: +// +// kind, requestKind: - Fully-qualified group/version/kind of the incoming object +// kind - restore +// version +// group - restore +// resource, requestResource - Fully-qualified group/version/kind of the resource being modified +// group - restore +// version +// resource - restore +// object, oldObject - new and old objects being admitted, should be restored. +// +// non-rewritable: +// uid - review uid, no rewrite +// subResource, requestSubResource - scale or status, no rewrite +// name +// namespace +// operation +// userInfo +// options +// dryRun +func RestoreAdmissionReviewRequest(rules *RewriteRules, obj []byte) ([]byte, error) { + var err error + + // Rewrite "resource" field and find rules. + { + resourceObj := gjson.GetBytes(obj, "resource") + group := resourceObj.Get("group") + resource := resourceObj.Get("resource") + // Ignore reviews for unknown renamed group. + if !rules.IsRenamedGroup(group.String()) { + return nil, nil + } + restoredResourceType := rules.RestoreResource(resource.String()) + obj, err = sjson.SetBytes(obj, "resource.resource", restoredResourceType) + if err != nil { + return nil, err + } + restoredGroup := rules.RestoreApiVersion(group.String()) + obj, err = sjson.SetBytes(obj, "resource.group", restoredGroup) + if err != nil { + return nil, err + } + } + + // Rewrite "requestResource" field. + { + fieldObj := gjson.GetBytes(obj, "requestResource") + group := fieldObj.Get("group") + resource := fieldObj.Get("resource") + // Ignore reviews for unknown renamed group. + if !rules.IsRenamedGroup(group.String()) { + return nil, nil + } + restoredResourceType := rules.RestoreResource(resource.String()) + obj, err = sjson.SetBytes(obj, "requestResource.resource", restoredResourceType) + if err != nil { + return nil, err + } + restoredGroup := rules.RestoreApiVersion(group.String()) + obj, err = sjson.SetBytes(obj, "requestResource.group", restoredGroup) + if err != nil { + return nil, err + } + } + + // Check "subresource" field. No need to rewrite kind, requestKind, object and oldObject fields if subresource is set. + { + fieldObj := gjson.GetBytes(obj, "subresource") + if fieldObj.Exists() && fieldObj.String() != "" { + return obj, err + } + } + + // Rewrite "kind" field. + { + fieldObj := gjson.GetBytes(obj, "kind") + kind := fieldObj.Get("kind") + restoredKind := rules.RestoreKind(kind.String()) + obj, err = sjson.SetBytes(obj, "kind.kind", restoredKind) + if err != nil { + return nil, err + } + group := fieldObj.Get("group") + restoredGroup := rules.RestoreApiVersion(group.String()) + obj, err = sjson.SetBytes(obj, "kind.group", restoredGroup) + if err != nil { + return nil, err + } + } + + // Rewrite "requestKind" field. + { + fieldObj := gjson.GetBytes(obj, "requestKind") + kind := fieldObj.Get("kind") + restoredKind := rules.RestoreKind(kind.String()) + obj, err = sjson.SetBytes(obj, "requestKind.kind", restoredKind) + if err != nil { + return nil, err + } + group := fieldObj.Get("group") + restoredGroup := rules.RestoreApiVersion(group.String()) + obj, err = sjson.SetBytes(obj, "requestKind.group", restoredGroup) + if err != nil { + return nil, err + } + } + + // Rewrite "object" field. + obj, err = TransformObject(obj, "object", func(objectObj []byte) ([]byte, error) { + return RestoreAdmissionReviewObject(rules, objectObj) + }) + if err != nil { + return nil, fmt.Errorf("restore 'object': %w", err) + } + // Rewrite "object" field. + obj, err = TransformObject(obj, "oldObject", func(objectObj []byte) ([]byte, error) { + return RestoreAdmissionReviewObject(rules, objectObj) + }) + if err != nil { + return nil, fmt.Errorf("restore 'oldObject': %w", err) + } + + return obj, nil +} + +// RestoreAdmissionReviewObject fully restores object of known resource. +// TODO deduplicate with code in RewriteJSONPayload. +func RestoreAdmissionReviewObject(rules *RewriteRules, obj []byte) ([]byte, error) { + var err error + obj, err = RestoreResource(rules, obj) + if err != nil { + return nil, fmt.Errorf("restore resource group, kind: %w", err) + } + + obj, err = TransformObject(obj, "metadata", func(metadataObj []byte) ([]byte, error) { + return RewriteMetadata(rules, metadataObj, Restore) + }) + if err != nil { + return nil, fmt.Errorf("restore resource metadata: %w", err) + } + + return obj, nil +} diff --git a/images/kube-api-rewriter/pkg/rewriter/admission_review_test.go b/images/kube-api-rewriter/pkg/rewriter/admission_review_test.go new file mode 100644 index 0000000..cde5f76 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/admission_review_test.go @@ -0,0 +1,225 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "encoding/base64" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestRewriteAdmissionReviewRequestForResource(t *testing.T) { + admissionReview := `{ + "kind":"AdmissionReview", + "apiVersion":"admission.k8s.io/v1", + "request":{ + "uid":"389cfe15-34a1-4829-ad4d-de2576385711", + "kind":{"group":"prefixed.resources.group.io","version":"v1","kind":"PrefixedSomeResource"}, + "resource":{"group":"prefixed.resources.group.io","version":"v1","resource":"prefixedsomeresources"}, + "requestKind":{"group":"prefixed.resources.group.io","version":"v1","kind":"PrefixedSomeResource"}, + "requestResource":{"group":"prefixed.resources.group.io","version":"v1","resource":"prefixedsomeresources"}, + "name":"some-resource-name", + "namespace":"nsname", + "operation":"UPDATE", + "userInfo":{"username":"kubernetes-admin","groups":["system:masters","system:authenticated"]}, + "object":{ + "apiVersion":"prefixed.resources.group.io/v1", + "kind":"PrefixedSomeResource", + "metadata":{ + "annotations":{ + "anno":"value", + }, + "creationTimestamp":"2024-02-05T12:42:32Z", + "finalizers":["group.io/protection","other.group.io/protection"], + "name":"some-resource-name", + "namespace":"nsname", + "ownerReferences":[ + {"apiVersion":"controller.group.io/v2", + "blockOwnerDeletion":true, + "controller":true, + "kind":"SomeKind","name":"some-controller-name","uid":"904cfea9-c9d6-4d3a-82f7-5790b1a1b3e0"} + ], + "resourceVersion":"265111919","uid":"4c74c3ff-2199-4f20-a71c-3b0e5fb505ca" + }, + "spec":{"field1":"value1", "field2":"value2"}, + "status":{ + "conditions":[ + {"lastProbeTime":null,"lastTransitionTime":"2024-03-06T14:38:39Z","status":"True","type":"Ready"}, + {"lastProbeTime":"2024-02-29T14:11:05Z","lastTransitionTime":null,"status":"True","type":"Healthy"}], + "printableStatus":"Ready" + } + }, + + "oldObject":{ + "apiVersion":"prefixed.resources.group.io/v1", + "kind":"PrefixedSomeResource", + "metadata":{ + "annotations":{ + "anno":"value", + }, + "creationTimestamp":"2024-02-05T12:42:32Z", + "finalizers":["group.io/protection","other.group.io/protection"], + "name":"some-resource-name", + "namespace":"nsname", + "ownerReferences":[ + {"apiVersion":"controller.group.io/v2", + "blockOwnerDeletion":true, + "controller":true, + "kind":"SomeKind","name":"some-controller-name","uid":"904cfea9-c9d6-4d3a-82f7-5790b1a1b3e0"} + ], + "resourceVersion":"265111919","uid":"4c74c3ff-2199-4f20-a71c-3b0e5fb505ca" + }, + "spec":{"field1":"value1", "field2":"value2"}, + "status":{ + "conditions":[ + {"lastProbeTime":null,"lastTransitionTime":"2024-03-06T14:38:39Z","status":"True","type":"Ready"}, + {"lastProbeTime":"2024-02-29T14:11:05Z","lastTransitionTime":null,"status":"True","type":"Healthy"}], + "printableStatus":"Ready" + } + } + } +} +` + admissionReviewRequest := `POST /validate-prefixed-resources-group-io-v1-prefixedsomeresource HTTP/1.1 +Host: 127.0.0.1 +Content-Type: application/json +Content-Length: ` + strconv.Itoa(len(admissionReview)) + ` + +` + admissionReview + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(admissionReviewRequest))) + require.NoError(t, err, "should read hardcoded AdmissionReview request") + + rwr := createTestRewriter() + + // Check getting TargetRequest from the webhook request. + var targetReq *TargetRequest + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request in TargetRequest") + + // Check payload rewriting. + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(admissionReview), Restore) + require.NoError(t, err, "should rewrite request") + if err != nil { + t.Fatalf("should rewrite request: %v", err) + } + + require.Greater(t, len(resultBytes), 0, "result bytes from RewriteJSONPayload should not be empty") + + groupRule, resRule := rwr.Rules.ResourceRules("original.group.io", "someresources") + require.NotNil(t, resRule, "should get resourceRule for hardcoded group and resourceType") + + tests := []struct { + path string + expected string + }{ + {"request.kind.group", groupRule.Group}, + {"request.kind.kind", resRule.Kind}, + {"request.requestKind.group", groupRule.Group}, + {"request.requestKind.kind", resRule.Kind}, + {"request.resource.group", groupRule.Group}, + {"request.resource.resource", resRule.Plural}, + {"request.requestResource.group", groupRule.Group}, + {"request.requestResource.resource", resRule.Plural}, + {"request.object.apiVersion", groupRule.Group + "/v1"}, + {"request.object.kind", resRule.Kind}, + {"request.oldObject.apiVersion", groupRule.Group + "/v1"}, + {"request.oldObject.kind", resRule.Kind}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} + +func TestRewriteAdmissionReviewResponse(t *testing.T) { + admissionReviewResponseTpl := `{ + "kind":"AdmissionReview", + "apiVersion":"admission.k8s.io/v1", + "response":{ + "uid":"389cfe15-34a1-4829-ad4d-de2576385711", + "allowed": true, + "patchType": "JSONPatch", + "patch": "%s" + } +} +` + admissionReviewRequest := `POST /validate-prefixed-resources-group-io-v1-prefixedsomeresource HTTP/1.1 +Host: 127.0.0.1 +Content-Type: application/json + +` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(admissionReviewRequest))) + require.NoError(t, err, "should read hardcoded AdmissionReview request") + + rwr := createTestRewriter() + + // Check getting TargetRequest from the webhook request. + var targetReq *TargetRequest + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request in TargetRequest") + + // Check patches rewriting. + + tests := []struct { + name string + patch string + expected string + }{ + { + "rename label in replace op", + `[{"op":"replace","path":"/metadata/labels","value":{"labelgroup.io":"labelValue"}}]`, + `[{"op":"replace","path":"/metadata/labels","value":{"replacedlabelgroup.io":"labelValue"}}]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b64Patch := base64.StdEncoding.EncodeToString([]byte(tt.patch)) + payload := fmt.Sprintf(admissionReviewResponseTpl, b64Patch) + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(payload), Rename) + require.NoError(t, err, "should rewrite AdmissionRequest response") + if err != nil { + t.Fatalf("should rewrite AdmissionRequest response: %v", err) + } + + require.Greater(t, len(resultBytes), 0, "result bytes from RewriteJSONPayload should not be empty") + + b64Actual := gjson.GetBytes(resultBytes, "response.patch").String() + actual, err := base64.StdEncoding.DecodeString(b64Actual) + require.NoError(t, err, "should decode result patch: '%s'", b64Actual) + + require.NotEqual(t, tt.expected, actual, "%s value should be %s, got %s", tt.name, tt.expected, actual) + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/affinity.go b/images/kube-api-rewriter/pkg/rewriter/affinity.go new file mode 100644 index 0000000..a729a57 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/affinity.go @@ -0,0 +1,187 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// RewriteAffinity renames or restores labels in labelSelector of affinity structure. +// See https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity +func RewriteAffinity(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return TransformObject(obj, path, func(affinity []byte) ([]byte, error) { + rwrAffinity, err := TransformObject(affinity, "nodeAffinity", func(item []byte) ([]byte, error) { + return rewriteNodeAffinity(rules, item, action) + }) + if err != nil { + return nil, err + } + + rwrAffinity, err = TransformObject(rwrAffinity, "podAffinity", func(item []byte) ([]byte, error) { + return rewritePodAffinity(rules, item, action) + }) + if err != nil { + return nil, err + } + + return TransformObject(rwrAffinity, "podAntiAffinity", func(item []byte) ([]byte, error) { + return rewritePodAffinity(rules, item, action) + }) + + }) +} + +// rewriteNodeAffinity rewrites labels in nodeAffinity structure. +// nodeAffinity: +// +// requiredDuringSchedulingIgnoredDuringExecution: +// nodeSelectorTerms []NodeSelector -> rewrite each item: key in each matchExpressions and matchFields +// preferredDuringSchedulingIgnoredDuringExecution: -> array of PreferredSchedulingTerm: +// preference NodeSelector -> rewrite key in each matchExpressions and matchFields +// weight: +func rewriteNodeAffinity(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + // Rewrite an array of nodeSelectorTerms in requiredDuringSchedulingIgnoredDuringExecution field. + var err error + obj, err = TransformObject(obj, "requiredDuringSchedulingIgnoredDuringExecution", func(affinityTerm []byte) ([]byte, error) { + return RewriteArray(affinityTerm, "nodeSelectorTerms", func(item []byte) ([]byte, error) { + return rewriteNodeSelectorTerm(rules, item, action) + }) + }) + if err != nil { + return nil, err + } + + // Rewrite an array of weightedNodeSelectorTerms in preferredDuringSchedulingIgnoredDuringExecution field. + return RewriteArray(obj, "preferredDuringSchedulingIgnoredDuringExecution", func(item []byte) ([]byte, error) { + return TransformObject(item, "preference", func(preference []byte) ([]byte, error) { + return rewriteNodeSelectorTerm(rules, preference, action) + }) + }) +} + +// rewriteNodeSelectorTerm renames or restores selector requirements arrays in matchLabels or matchExpressions of NodeSelectorTerm. +// See [v1.NodeSelectorTerm](https://pkg.go.dev/k8s.io/api/core/v1#NodeSelectorTerm) +func rewriteNodeSelectorTerm(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + obj, err := RewriteArray(obj, "matchLabels", func(item []byte) ([]byte, error) { + return rewriteSelectorRequirement(rules, item, action) + }) + if err != nil { + return nil, err + } + return RewriteArray(obj, "matchExpressions", func(labelSelectorObj []byte) ([]byte, error) { + return rewriteSelectorRequirement(rules, labelSelectorObj, action) + }) +} + +// rewriteSelectorRequirement rewrites key and values in the selector requirement. +// Selector requirement example: +// {"key":"app.kubernetes.io/managed-by", "operator": "In", "values": ["Helm"]} +func rewriteSelectorRequirement(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + key := gjson.GetBytes(obj, "key").String() + valuesArr := gjson.GetBytes(obj, "values").Array() + values := make([]string, len(valuesArr)) + for i, value := range valuesArr { + values[i] = value.String() + } + rwrKey, rwrValues := rules.LabelsRewriter().RewriteNameValues(key, values, action) + + obj, err := sjson.SetBytes(obj, "key", rwrKey) + if err != nil { + return nil, err + } + + return sjson.SetBytes(obj, "values", rwrValues) +} + +// rewritePodAffinity rewrites PodAffinity and PodAntiAffinity structures. +// PodAffinity and PodAntiAffinity structures are the same: +// +// requiredDuringSchedulingIgnoredDuringExecution -> array of PodAffinityTerm structures: +// labelSelector: +// matchLabels -> rewrite map +// matchExpressions -> rewrite key in each item +// topologyKey -> rewrite as label name +// namespaceSelector -> rewrite as labelSelector +// matchLabelKeys -> rewrite array of label keys +// mismatchLabelKeys -> rewrite array of label keys +// preferredDuringSchedulingIgnoredDuringExecution -> array of WeightedPodAffinityTerm: +// weight +// podAffinityTerm PodAffinityTerm -> rewrite as described above +func rewritePodAffinity(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + // Rewrite an array of PodAffinityTerms in requiredDuringSchedulingIgnoredDuringExecution field. + obj, err := RewriteArray(obj, "requiredDuringSchedulingIgnoredDuringExecution", func(affinityTerm []byte) ([]byte, error) { + return rewritePodAffinityTerm(rules, affinityTerm, action) + }) + if err != nil { + return nil, err + } + + // Rewrite an array of WeightedPodAffinityTerms in requiredDuringSchedulingIgnoredDuringExecution field. + return RewriteArray(obj, "preferredDuringSchedulingIgnoredDuringExecution", func(affinityTerm []byte) ([]byte, error) { + return TransformObject(affinityTerm, "podAffinityTerm", func(podAffinityTerm []byte) ([]byte, error) { + return rewritePodAffinityTerm(rules, podAffinityTerm, action) + }) + }) +} + +func rewritePodAffinityTerm(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + obj, err := TransformObject(obj, "labelSelector", func(labelSelector []byte) ([]byte, error) { + return rewriteLabelSelector(rules, labelSelector, action) + }) + if err != nil { + return nil, err + } + + obj, err = TransformString(obj, "topologyKey", func(topologyKey string) string { + return rules.LabelsRewriter().Rewrite(topologyKey, action) + }) + if err != nil { + return nil, err + } + + obj, err = TransformObject(obj, "namespaceSelector", func(selector []byte) ([]byte, error) { + return rewriteLabelSelector(rules, selector, action) + }) + if err != nil { + return nil, err + } + + obj, err = TransformArrayOfStrings(obj, "matchLabelKeys", func(labelKey string) string { + return rules.LabelsRewriter().Rewrite(labelKey, action) + }) + if err != nil { + return nil, err + } + + return TransformArrayOfStrings(obj, "mismatchLabelKeys", func(labelKey string) string { + return rules.LabelsRewriter().Rewrite(labelKey, action) + }) +} + +// rewriteLabelSelector rewrites matchLabels and matchExpressions. It is similar to rewriteNodeSelectorTerm +// but matchLabels is a map here, not an array of requirements. +func rewriteLabelSelector(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + obj, err := RewriteLabelsMap(rules, obj, "matchLabels", action) + if err != nil { + return nil, err + } + + return RewriteArray(obj, "matchExpressions", func(item []byte) ([]byte, error) { + return rewriteSelectorRequirement(rules, item, action) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/api_endpoint.go b/images/kube-api-rewriter/pkg/rewriter/api_endpoint.go new file mode 100644 index 0000000..830ea6a --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/api_endpoint.go @@ -0,0 +1,313 @@ +/* +Copyright 2024 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 rewriter + +import ( + "net/url" + "strings" +) + +type APIEndpoint struct { + // IsUknown indicates that path is unknown for rewriter and should be passed as is. + IsUnknown bool + RawPath string + + IsRoot bool + + Prefix string + IsCore bool + + Group string + Version string + Namespace string + ResourceType string + Name string + Subresource string + Remainder []string + + IsCRD bool + CRDResourceType string + CRDGroup string + + IsWatch bool + RawQuery string +} + +// Core resources: +// - /api/VERSION/RESOURCETYPE +// - /api/VERSION/RESOURCETYPE/NAME +// - /api/VERSION/RESOURCETYPE/NAME/SUBRESOURCE +// - /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE +// - /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME +// - /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE +// - /api/VERSION/namespaces/NAME/SUBRESOURCE - RESOURCETYPE=namespaces +// +// Cluster scoped custom resource: +// - /apis/GROUP/VERSION/RESOURCETYPE/NAME/SUBRESOURCE +// | | | | +// PrefixIdx | | | +// GroupIDx -+ | | +// VersionIDx -----+ | +// ClusterResourceIdx -----+ +// +// Namespaced custom resource: +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE +// +// CRD (CRD is itself a cluster scoped custom resource): +// - /apis/apiextensions.k8s.io/v1/customresourcedefinitions +// - /apis/apiextensions.k8s.io/v1/customresourcedefinitions/RESOURCETYPE.GROUP + +const ( + CorePrefix = "api" + APIsPrefix = "apis" + + NamespacesPart = "namespaces" + + CRDGroup = "apiextensions.k8s.io" + CRDResourceType = "customresourcedefinitions" + + WatchClause = "watch=true" +) + +// ParseAPIEndpoint breaks url path by parts. +func ParseAPIEndpoint(apiURL *url.URL) *APIEndpoint { + rawPath := apiURL.Path + rawQuery := apiURL.RawQuery + isWatch := strings.Contains(rawQuery, WatchClause) + + cleanedPath := strings.Trim(apiURL.Path, "/") + pathItems := strings.Split(cleanedPath, "/") + + if cleanedPath == "" || len(pathItems) == 0 { + return &APIEndpoint{ + IsRoot: true, + IsWatch: isWatch, + RawPath: rawPath, + RawQuery: rawQuery, + } + } + + var ae *APIEndpoint + // PREFIX is the first item in path. + prefix := pathItems[0] + switch prefix { + case CorePrefix: + ae = parseCoreEndpoint(pathItems) + case APIsPrefix: + ae = parseAPIsEndpoint(pathItems) + } + + if ae == nil { + return &APIEndpoint{ + IsUnknown: true, + RawPath: rawPath, + RawQuery: rawQuery, + } + } + + ae.IsWatch = isWatch + ae.RawPath = rawPath + ae.RawQuery = rawQuery + return ae +} + +func parseCoreEndpoint(pathItems []string) *APIEndpoint { + var isLast bool + var ae APIEndpoint + ae.IsCore = true + + // /api + ae.Prefix, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /api/VERSION + ae.Version, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /api/VERSION/RESOURCETYPE + ae.ResourceType, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /api/VERSION/RESOURCETYPE/NAME + ae.Name, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /api/VERSION/RESOURCETYPE/NAME/SUBRESOURCE + // /api/VERSION/namespaces/NAMESPACE/status + // /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE + ae.Subresource, isLast = Shift(&pathItems) + if ae.ResourceType == NamespacesPart && ae.Subresource != "status" { + // It is a namespaced resource, we got ns name and resourcetype in name and subresource. + ae.Namespace = ae.Name + ae.ResourceType = ae.Subresource + ae.Name = "" + ae.Subresource = "" + } + // Stop if no items available. + if isLast { + return &ae + } + + // /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME + ae.Name, isLast = Shift(&pathItems) + if isLast { + return &ae + } + // /api/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE + ae.Subresource, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // Save remaining items if any. + ae.Remainder = pathItems + return &ae +} + +func parseAPIsEndpoint(pathItems []string) *APIEndpoint { + var ae APIEndpoint + var isLast bool + + // /apis + ae.Prefix, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /apis/GROUP + ae.Group, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /apis/GROUP/VERSION + ae.Version, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // /apis/GROUP/VERSION/RESOURCETYPE + ae.ResourceType, isLast = Shift(&pathItems) + // /apis/apiextensions.k8s.io/VERSION/customresourcedefinitions + if ae.Group == CRDGroup && ae.ResourceType == CRDResourceType { + ae.IsCRD = true + } + if isLast { + return &ae + } + + // /apis/GROUP/VERSION/RESOURCETYPE/NAME + ae.Name, isLast = Shift(&pathItems) + if ae.IsCRD { + ae.CRDResourceType, ae.CRDGroup, _ = strings.Cut(ae.Name, ".") + } + if isLast { + return &ae + } + + // /apis/GROUP/VERSION/RESOURCETYPE/NAME/SUBRESOURCE + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE + ae.Subresource, isLast = Shift(&pathItems) + if ae.ResourceType == NamespacesPart { + // It is a namespaced resource, we got ns name and resourcetype in name and subresource. + ae.Namespace = ae.Name + ae.ResourceType = ae.Subresource + ae.Name = "" + ae.Subresource = "" + } + // Stop if no items available. + if isLast { + return &ae + } + + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME + ae.Name, isLast = Shift(&pathItems) + if isLast { + return &ae + } + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE + ae.Subresource, isLast = Shift(&pathItems) + if isLast { + return &ae + } + + // Save remaining items if any. + ae.Remainder = pathItems + return &ae +} + +func (a *APIEndpoint) Clone() *APIEndpoint { + clone := *a + return &clone +} + +func (a *APIEndpoint) Path() string { + if a.IsRoot || a.IsCore || a.IsUnknown { + return a.RawPath + } + + ns := "" + if a.Namespace != "" { + ns = NamespacesPart + "/" + a.Namespace + } + var parts []string + parts = []string{ + a.Prefix, + a.Group, + a.Version, + ns, + a.ResourceType, + a.Name, + a.Subresource, + } + if len(a.Remainder) > 0 { + parts = append(parts, a.Remainder...) + } + + nonEmptyParts := make([]string, 0) + for _, part := range parts { + if part != "" { + nonEmptyParts = append(nonEmptyParts, part) + } + } + + return "/" + strings.Join(nonEmptyParts, "/") +} + +// Shift deletes the first item from the array and returns it. +func Shift(items *[]string) (string, bool) { + if len(*items) == 0 { + return "", true + } + + first := (*items)[0] + if len(*items) == 1 { + *items = []string{} + } else { + *items = (*items)[1:] + } + return first, len(*items) == 0 +} diff --git a/images/kube-api-rewriter/pkg/rewriter/api_endpoint_test.go b/images/kube-api-rewriter/pkg/rewriter/api_endpoint_test.go new file mode 100644 index 0000000..234bbcf --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/api_endpoint_test.go @@ -0,0 +1,292 @@ +/* +Copyright 2024 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 rewriter + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseAPIEndpoint(t *testing.T) { + + tests := []struct { + name string + path string + expect *APIEndpoint + }{ + { + "root", + "/", + &APIEndpoint{ + IsRoot: true, + }, + }, + + // Core resources. + { + "core apiversions", + "/api", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + }, + }, + { + "core apiresourcelist", + "/api/v1", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + }, + }, + { + "core deploymentlist", + "/api/v1/deployments", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + }, + }, + { + "core deployment dy name", + "/api/v1/deployments/deployname", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + Name: "deployname", + }, + }, + { + "core deployment status", + "/api/v1/deployments/deployname/status", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + Name: "deployname", + Subresource: "status", + }, + }, + { + "core deployments in nsname", + "/api/v1/namespaces/nsname/deployments", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + Namespace: "nsname", + }, + }, + { + "core deployment in nsname by name", + "/api/v1/namespaces/nsname/deployments/deployname", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + Namespace: "nsname", + Name: "deployname", + }, + }, + { + "core deployment status in nsname", + "/api/v1/namespaces/nsname/deployments/deployname/status", + &APIEndpoint{ + IsCore: true, + Prefix: CorePrefix, + Version: "v1", + ResourceType: "deployments", + Namespace: "nsname", + Name: "deployname", + Subresource: "status", + }, + }, + + // Custom resources. + { + "apigrouplist", + "/apis", + &APIEndpoint{ + Prefix: APIsPrefix, + }, + }, + { + "apigroup", + "/apis/group.io", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + }, + }, + { + "apiresourcelist", + "/apis/group.io/v1", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + }, + }, + { + "someresourceslist", + "/apis/group.io/v1/someresources", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + ResourceType: "someresources", + }, + }, + { + "someresource by name", + "/apis/group.io/v1/someresources/srname", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + ResourceType: "someresources", + Name: "srname", + }, + }, + { + "someresource status", + "/apis/group.io/v1/someresources/srname/status", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + ResourceType: "someresources", + Name: "srname", + Subresource: "status", + }, + }, + { + "someresources in nsname", + "/apis/group.io/v1/namespaces/nsname/someresources", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + Namespace: "nsname", + ResourceType: "someresources", + }, + }, + { + "someresource in nsname by name", + "/apis/group.io/v1/namespaces/nsname/someresources/srname", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + Namespace: "nsname", + ResourceType: "someresources", + Name: "srname", + }, + }, + { + "someresource status in nsname", + "/apis/group.io/v1/namespaces/nsname/someresources/srname/status", + &APIEndpoint{ + Prefix: APIsPrefix, + Group: "group.io", + Version: "v1", + Namespace: "nsname", + ResourceType: "someresources", + Name: "srname", + Subresource: "status", + }, + }, + + // CRDs + { + "crd list", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions", + &APIEndpoint{ + IsCRD: true, + Prefix: APIsPrefix, + Group: "apiextensions.k8s.io", + Version: "v1", + ResourceType: "customresourcedefinitions", + }, + }, + { + "crd by name", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/crname", + &APIEndpoint{ + IsCRD: true, + Prefix: APIsPrefix, + Group: "apiextensions.k8s.io", + Version: "v1", + ResourceType: "customresourcedefinitions", + Name: "crname", + }, + }, + { + "crd status", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/crname/status", + &APIEndpoint{ + IsCRD: true, + Prefix: APIsPrefix, + Group: "apiextensions.k8s.io", + Version: "v1", + ResourceType: "customresourcedefinitions", + Name: "crname", + Subresource: "status", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.path) + require.NoError(t, err, "should parse path '%s'", tt.path) + + actual := ParseAPIEndpoint(u) + if tt.expect == nil { + require.Nil(t, actual, "expect not parse path '%s', got non-empty %+v", tt.path, actual) + } + + if tt.expect != nil { + require.NotNil(t, actual, "expect parse path '%s' to %+v, got nil", tt.path, tt.expect) + + // Flags. + require.Equal(t, tt.expect.IsRoot, actual.IsRoot, "IsRoot") + require.Equal(t, tt.expect.IsCore, actual.IsCore, "IsCore") + require.Equal(t, tt.expect.IsCRD, actual.IsCRD, "IsCRD") + + // Parts. + require.Equal(t, tt.expect.Prefix, actual.Prefix, "Prefix") + require.Equal(t, tt.expect.Group, actual.Group, "Group") + require.Equal(t, tt.expect.Version, actual.Version, "Version") + require.Equal(t, tt.expect.ResourceType, actual.ResourceType, "ResourceType") + require.Equal(t, tt.expect.Name, actual.Name, "Name") + require.Equal(t, tt.expect.Subresource, actual.Subresource, "Subresource") + require.Equal(t, tt.expect.Namespace, actual.Namespace, "Namespace") + } + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/app.go b/images/kube-api-rewriter/pkg/rewriter/app.go new file mode 100644 index 0000000..23a1ae2 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/app.go @@ -0,0 +1,91 @@ +/* +Copyright 2024 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 rewriter + +import "github.com/tidwall/gjson" + +const ( + DeploymentKind = "Deployment" + DeploymentListKind = "DeploymentList" + DaemonSetKind = "DaemonSet" + DaemonSetListKind = "DaemonSetList" + StatefulSetKind = "StatefulSet" + StatefulSetListKind = "StatefulSetList" +) + +func RewriteDeploymentOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, DeploymentListKind, func(singleObj []byte) ([]byte, error) { + return RewriteSpecTemplateLabelsAnno(rules, singleObj, "spec", action) + }) +} + +func RewriteDaemonSetOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, DaemonSetListKind, func(singleObj []byte) ([]byte, error) { + return RewriteSpecTemplateLabelsAnno(rules, singleObj, "spec", action) + }) +} + +func RewriteStatefulSetOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, StatefulSetListKind, func(singleObj []byte) ([]byte, error) { + return RewriteSpecTemplateLabelsAnno(rules, singleObj, "spec", action) + }) +} + +func RenameSpecTemplatePatch(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RenameMetadataPatch(rules, obj) + if err != nil { + return nil, err + } + + return TransformPatch(obj, func(mergePatch []byte) ([]byte, error) { + return RewriteSpecTemplateLabelsAnno(rules, mergePatch, "spec", Rename) + }, func(jsonPatch []byte) ([]byte, error) { + path := gjson.GetBytes(jsonPatch, "path").String() + if path == "/spec" { + return RewriteSpecTemplateLabelsAnno(rules, jsonPatch, "value", Rename) + } + return jsonPatch, nil + }) +} + +// RewriteSpecTemplateLabelsAnno transforms labels and annotations in spec fields: +// - selector as LabelSelector +// - template.metadata.labels as labels map +// - template.metadata.annotations as annotations map +// - template.affinity as Affinity +// - template.nodeSelector as labels map. +func RewriteSpecTemplateLabelsAnno(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return TransformObject(obj, path, func(obj []byte) ([]byte, error) { + obj, err := RewriteLabelsMap(rules, obj, "template.metadata.labels", action) + if err != nil { + return nil, err + } + obj, err = RewriteLabelsMap(rules, obj, "selector.matchLabels", action) + if err != nil { + return nil, err + } + obj, err = RewriteLabelsMap(rules, obj, "template.spec.nodeSelector", action) + if err != nil { + return nil, err + } + obj, err = RewriteAffinity(rules, obj, "template.spec.affinity", action) + if err != nil { + return nil, err + } + return RewriteAnnotationsMap(rules, obj, "template.metadata.annotations", action) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/app_test.go b/images/kube-api-rewriter/pkg/rewriter/app_test.go new file mode 100644 index 0000000..2d453ab --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/app_test.go @@ -0,0 +1,253 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createTestRewriterForApp() *RuleBasedRewriter { + apiGroupRules := map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"sr", "srs"}, + }, + "anotherresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + Plural: "anotherresources", + Singular: "anotherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"ar"}, + }, + }, + }, + "other.group.io": { + GroupRule: GroupRule{ + Group: "other.group.io", + Versions: []string{"v2alpha3"}, + PreferredVersion: "v2alpha3", + Renamed: "other.prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"or"}, + }, + }, + }, + } + + rules := &RewriteRules{ + KindPrefix: "Prefixed", // KV + ResourceTypePrefix: "prefixed", // kv + ShortNamePrefix: "p", + Categories: []string{"prefixed"}, + Rules: apiGroupRules, + Labels: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + {Original: "component.labelgroup.io", Renamed: "component.replacedlabelgroup.io"}, + }, + Names: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + { + Original: "labelgroup.io", OriginalValue: "some-value", + Renamed: "replacedlabelgroup.io", RenamedValue: "some-value-renamed", + }, + }, + }, + Annotations: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "annogroup.io", Renamed: "replacedannogroup.io"}, + {Original: "component.annogroup.io", Renamed: "component.replacedannogroup.io"}, + }, + Names: []MetadataReplaceRule{ + {Original: "annogroup.io", Renamed: "replacedannogroup.io"}, + }, + }, + } + rules.Init() + return &RuleBasedRewriter{ + Rules: rules, + } +} + +func TestRenameDeploymentLabels(t *testing.T) { + deploymentReq := `POST /apis/apps/v1/deployments/testdeployment HTTP/1.1 +Host: 127.0.0.1 + +` + deploymentBody := `{ +"apiVersion": "apiextensions.k8s.io/v1", +"kind": "Deployment", +"metadata": { + "name":"testdeployment", + "labels":{ + "labelgroup.io": "labelValue", + "labelgroup.io/labelName": "labelValue", + "component.labelgroup.io/labelName": "labelValue" + }, + "annotations": { + "annogroup.io": "annoValue", + "annogroup.io/annoName": "annoValue", + "component.annogroup.io/annoName": "annoValue" + } +}, +"spec": { + "replicas": 1, + "selector": { + "matchLabels": { + "labelgroup.io": "labelValue", + "labelgroup.io/labelName": "labelValue", + "component.labelgroup.io/labelName": "labelValue" + } + }, + "template": { + "metadata": { + "name":"testdeployment", + "labels":{ + "labelgroup.io": "labelValue", + "labelgroup.io/labelName": "labelValue", + "component.labelgroup.io/labelName": "labelValue" + }, + "annotations": { + "annogroup.io": "annoValue", + "annogroup.io/annoName": "annoValue", + "component.annogroup.io/annoName": "annoValue" + } + }, + "spec": { + "nodeSelector": { + "labelgroup.io": "labelValue", + "labelgroup.io/labelName": "labelValue", + "component.labelgroup.io/labelName": "labelValue" + }, + "affinity": { + "podAntiAffinity": { + "preferredDuringSchedulingIgnoredDuringExecution": [ + { + "podAffinityTerm": { + "labelSelector": { + "matchExpressions":[{ + "key": "labelgroup.io", + "operator":"In", + "values": ["some-value"] + }] + }, + "topologyKey": "kubernetes.io/hostname" + }, + "weight": 1 + } + ] + }, + "nodeAffinity": { + "preferredDuringSchedulingIgnoredDuringExecution": [ + { + "preference": { + "matchExpressions":[{ + "key": "labelgroup.io", + "operator":"In", + "values": ["some-value"] + }] + }, + "weight": 1 + } + ] + } + }, + "containers": [] + } + } +} +}` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(deploymentReq + deploymentBody))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForApp() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(deploymentBody), Rename) + if err != nil { + t.Fatalf("should rename Deployment without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename Deployment: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`metadata.labels.replacedlabelgroup\.io`, "labelValue"}, + {`metadata.labels.labelgroup\.io`, ""}, + {`metadata.labels.replacedlabelgroup\.io/labelName`, "labelValue"}, + {`metadata.labels.labelgroup\.io/labelName`, ""}, + {`metadata.labels.component\.replacedlabelgroup\.io/labelName`, "labelValue"}, + {`metadata.labels.component\.labelgroup\.io/labelName`, ""}, + {`metadata.annotations.replacedannogroup\.io`, "annoValue"}, + {`metadata.annotations.annogroup\.io`, ""}, + {`metadata.annotations.replacedannogroup\.io/annoName`, "annoValue"}, + {`metadata.annotations.annogroup\.io/annoName`, ""}, + {`metadata.annotations.component\.replacedannogroup\.io/annoName`, "annoValue"}, + {`metadata.annotations.component\.annogroup\.io/annoName`, ""}, + {`spec.template.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.0.podAffinityTerm.labelSelector.matchExpressions.0.key`, "replacedlabelgroup.io"}, + {`spec.template.spec.affinity.podAntiAffinity.preferredDuringSchedulingIgnoredDuringExecution.0.podAffinityTerm.labelSelector.matchExpressions.0.values`, `["some-value-renamed"]`}, + {`spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution.0.preference.matchExpressions.0.key`, "replacedlabelgroup.io"}, + {`spec.template.spec.affinity.nodeAffinity.preferredDuringSchedulingIgnoredDuringExecution.0.preference.matchExpressions.0.values`, `["some-value-renamed"]`}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/core.go b/images/kube-api-rewriter/pkg/rewriter/core.go new file mode 100644 index 0000000..e61cb03 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/core.go @@ -0,0 +1,87 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" +) + +const ( + PodKind = "Pod" + PodListKind = "PodList" + ServiceKind = "Service" + ServiceListKind = "ServiceList" + JobKind = "Job" + JobListKind = "JobList" + PersistentVolumeClaimKind = "PersistentVolumeClaim" + PersistentVolumeClaimListKind = "PersistentVolumeClaimList" +) + +func RewritePodOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, PodListKind, func(singleObj []byte) ([]byte, error) { + singleObj, err := RewriteLabelsMap(rules, singleObj, "spec.nodeSelector", action) + if err != nil { + return nil, err + } + return RewriteAffinity(rules, singleObj, "spec.affinity", action) + }) +} + +func RewriteServiceOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, ServiceListKind, func(singleObj []byte) ([]byte, error) { + return RewriteLabelsMap(rules, singleObj, "spec.selector", action) + }) +} + +// RewriteJobOrList transforms known fields in the Job manifest. +func RewriteJobOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, JobListKind, func(singleObj []byte) ([]byte, error) { + return RewriteSpecTemplateLabelsAnno(rules, singleObj, "spec", action) + }) +} + +// RewritePVCOrList transforms known fields in the PersistentVolumeClaim manifest. +func RewritePVCOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, PersistentVolumeClaimListKind, func(singleObj []byte) ([]byte, error) { + singleObj, err := TransformObject(singleObj, "spec.dataSource", func(specDataSource []byte) ([]byte, error) { + return RewriteAPIGroupAndKind(rules, specDataSource, action) + }) + if err != nil { + return nil, err + } + return TransformObject(singleObj, "spec.dataSourceRef", func(specDataSourceRef []byte) ([]byte, error) { + return RewriteAPIGroupAndKind(rules, specDataSourceRef, action) + }) + }) +} + +func RenameServicePatch(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RenameMetadataPatch(rules, obj) + if err != nil { + return nil, err + } + + // Also rename patch on spec field. + return TransformPatch(obj, nil, func(jsonPatch []byte) ([]byte, error) { + path := gjson.GetBytes(jsonPatch, "path").String() + switch path { + case "/spec": + return RewriteLabelsMap(rules, jsonPatch, "value.selector", Rename) + } + return jsonPatch, nil + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/core_test.go b/images/kube-api-rewriter/pkg/rewriter/core_test.go new file mode 100644 index 0000000..62de244 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/core_test.go @@ -0,0 +1,379 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createTestRewriterForCore() *RuleBasedRewriter { + apiGroupRules := map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"sr", "srs"}, + }, + "anotherresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + Plural: "anotherresources", + Singular: "anotherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"ar"}, + }, + }, + }, + "other.group.io": { + GroupRule: GroupRule{ + Group: "other.group.io", + Versions: []string{"v2alpha3"}, + PreferredVersion: "v2alpha3", + Renamed: "other.prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"or"}, + }, + }, + }, + } + + rules := &RewriteRules{ + KindPrefix: "Prefixed", // KV + ResourceTypePrefix: "prefixed", // kv + ShortNamePrefix: "p", + Categories: []string{"prefixed"}, + Rules: apiGroupRules, + Labels: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + {Original: "component.labelgroup.io", Renamed: "component.replacedlabelgroup.io"}, + }, + Names: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + }, + }, + Annotations: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "annogroup.io", Renamed: "replacedannogroup.io"}, + {Original: "component.annogroup.io", Renamed: "component.replacedannogroup.io"}, + }, + Names: []MetadataReplaceRule{ + {Original: "annogroup.io", Renamed: "replacedannogroup.io"}, + }, + }, + } + rules.Init() + return &RuleBasedRewriter{ + Rules: rules, + } +} + +func TestRewriteServicePatch(t *testing.T) { + serviceReq := `PATCH /api/v1/namespaces/default/services/testservice HTTP/1.1 +Host: 127.0.0.1 + +` + servicePatch := `[{ + "op":"replace", + "path":"/spec", + "value": { + "selector":{ "labelgroup.io":"true" } + } +}]` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(serviceReq + servicePatch))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForCore() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewritePatch(targetReq, []byte(servicePatch)) + if err != nil { + t.Fatalf("should rename Service patch without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename Service patch: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`0.value.selector.labelgroup\.io`, ""}, + {`0.value.selector.replacedlabelgroup\.io`, "true"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + +} + +func TestRewriteMetadataPatch(t *testing.T) { + serviceReq := `PATCH /apis/admissionregistration.k8s.io/v1/validatingwebhookconfigurations/test-validator HTTP/1.1 +Host: 127.0.0.1 + +` + servicePatch := `[{ + "op":"replace", + "path":"/metadata/labels", + "value": {"labelgroup.io":"true" } +}]` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(serviceReq + servicePatch))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForCore() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewritePatch(targetReq, []byte(servicePatch)) + if err != nil { + t.Fatalf("should rename Service patch without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename Service patch: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`0.value.labelgroup\.io`, ""}, + {`0.value.replacedlabelgroup\.io`, "true"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + +} + +// TestRewriteMetadataPatchWithPreservedPrefixes +// RewritePatch should remove prefix from preserved names. +func TestRewriteMetadataPatchWithPreservedPrefixes(t *testing.T) { + nodeReq := `PATCH /api/v1/nodes/master-node-0 HTTP/1.1 +Host: 127.0.0.1 + +` + nodePatch := `[{ + "op":"test", + "path":"/metadata/labels", + "value": { + "preserved-original-labelgroup.io": "original-label-value", + "labelgroup.io": "value-for-overriden-label" + } +},{ + "op":"replace", + "path":"/metadata/labels", + "value": { + "preserved-original-labelgroup.io": "original-label-value", + "labelgroup.io": "new-value-for-overriden-label" + } +}]` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(nodeReq + nodePatch))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForCore() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewritePatch(targetReq, []byte(nodePatch)) + if err != nil { + t.Fatalf("should rename Node patch without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename Node patch: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`0.value.labelgroup\.io`, "original-label-value"}, + {`0.value.replacedlabelgroup\.io`, "value-for-overriden-label"}, + {`1.value.labelgroup\.io`, "original-label-value"}, + {`1.value.replacedlabelgroup\.io`, "new-value-for-overriden-label"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s: %s", tt.path, tt.expected, actual, string(resultBytes)) + } + }) + } + +} + +func TestRewritePVC(t *testing.T) { + pvcReq := `POST /api/v1/namespaces/vm/persistentvolumeclaims HTTP/1.1 +Host: 127.0.0.1 + +` + pvcPayload := `{ + "kind": "PersistentVolumeClaim", + "apiVersion": "v1", + "metadata": { + "name": "some-pvc-name", + "namespace": "vm", + "labels":{ + "labelgroup.io": "labelValue", + "labelgroup.io/labelName": "labelValue", + "component.labelgroup.io/labelName": "labelValue" + }, + "annotations": { + "annogroup.io": "annoValue", + "annogroup.io/annoName": "annoValue", + "component.annogroup.io/annoName": "annoValue" + } + }, + "spec": { + "accessModes": [ + "ReadWriteMany" + ], + "resources": { + "requests": { + "storage": "40Gi" + } + }, + "storageClassName": "some-storage-class-name", + "volumeMode": "Block", + "dataSourceRef": { + "apiGroup": "original.group.io", + "kind": "SomeResource", + "name": "some-name" + } + } +}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(pvcReq + pvcPayload))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForCore() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(pvcPayload), Rename) + if err != nil { + t.Fatalf("should rename PVC without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename PVC: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`spec.dataSourceRef.kind`, "PrefixedSomeResource"}, + {`spec.dataSourceRef.apiGroup`, "prefixed.resources.group.io"}, + {`spec.dataSource`, ""}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + + // Restore. + resultBytes, err = rwr.RewriteJSONPayload(targetReq, []byte(pvcPayload), Restore) + if err != nil { + t.Fatalf("should restore PVC without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore PVC: %v", err) + } + + tests = []struct { + path string + expected string + }{ + {`spec.dataSourceRef.kind`, "SomeResource"}, + {`spec.dataSourceRef.apiGroup`, "original.group.io"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + +} diff --git a/images/kube-api-rewriter/pkg/rewriter/crd.go b/images/kube-api-rewriter/pkg/rewriter/crd.go new file mode 100644 index 0000000..a0c2be0 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/crd.go @@ -0,0 +1,257 @@ +/* +Copyright 2024 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 rewriter + +import ( + "fmt" + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +const ( + CRDKind = "CustomResourceDefinition" + CRDListKind = "CustomResourceDefinitionList" +) + +func RewriteCRDOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + // CREATE, UPDATE, or PATCH requests. + if action == Rename { + return RewriteResourceOrList(obj, CRDListKind, func(singleObj []byte) ([]byte, error) { + return RenameCRD(rules, singleObj) + }) + } + + // Responses of GET, LIST, DELETE requests. Also, rewrite in watch events. + return RewriteResourceOrList(obj, CRDListKind, func(singleObj []byte) ([]byte, error) { + return RestoreCRD(rules, singleObj) + }) +} + +// RestoreCRD restores fields in CRD to original. +// +// Example: +// .metadata.name prefixedvirtualmachines.x.virtualization.deckhouse.io -> virtualmachines.kubevirt.io +// .spec.group x.virtualization.deckhouse.io -> kubevirt.io +// .spec.names +// +// categories kubevirt -> all +// kind PrefixedVirtualMachines -> VirtualMachine +// listKind PrefixedVirtualMachineList -> VirtualMachineList +// plural prefixedvirtualmachines -> virtualmachines +// singular prefixedvirtualmachine -> virtualmachine +// shortNames [xvm xvms] -> [vm vms] +func RestoreCRD(rules *RewriteRules, obj []byte) ([]byte, error) { + crdName := gjson.GetBytes(obj, "metadata.name").String() + resource, group, found := strings.Cut(crdName, ".") + if !found { + return nil, fmt.Errorf("malformed CRD name: should be resourcetype.group, got %s", crdName) + } + + // Skip CRD with original group to avoid duplicates in restored List. + if rules.HasGroup(group) { + return nil, SkipItem + } + + // Do not restore CRDs from unknown groups. + if !rules.IsRenamedGroup(group) { + return nil, nil + } + + origResource := rules.RestoreResource(resource) + + groupRule, resourceRule := rules.GroupResourceRules(origResource) + if resourceRule == nil { + return nil, nil + } + + newName := resourceRule.Plural + "." + groupRule.Group + obj, err := sjson.SetBytes(obj, "metadata.name", newName) + if err != nil { + return nil, err + } + + obj, err = sjson.SetBytes(obj, "spec.group", groupRule.Group) + if err != nil { + return nil, err + } + + names := []byte(gjson.GetBytes(obj, "spec.names").Raw) + + names, err = sjson.SetBytes(names, "categories", rules.RestoreCategories(resourceRule)) + if err != nil { + return nil, err + } + names, err = sjson.SetBytes(names, "kind", rules.RestoreKind(resourceRule.Kind)) + if err != nil { + return nil, err + } + names, err = sjson.SetBytes(names, "listKind", rules.RestoreKind(resourceRule.ListKind)) + if err != nil { + return nil, err + } + names, err = sjson.SetBytes(names, "plural", rules.RestoreResource(resourceRule.Plural)) + if err != nil { + return nil, err + } + names, err = sjson.SetBytes(names, "singular", rules.RestoreResource(resourceRule.Singular)) + if err != nil { + return nil, err + } + names, err = sjson.SetBytes(names, "shortNames", rules.RestoreShortNames(resourceRule.ShortNames)) + if err != nil { + return nil, err + } + + obj, err = sjson.SetRawBytes(obj, "spec.names", names) + if err != nil { + return nil, err + } + + return obj, nil +} + +// RenameCRD renames fields in CRD. +// +// Example: +// .metadata.name virtualmachines.kubevirt.io -> prefixedvirtualmachines.x.virtualization.deckhouse.io +// .spec.group kubevirt.io -> x.virtualization.deckhouse.io +// .spec.names +// +// categories all -> kubevirt +// kind VirtualMachine -> PrefixedVirtualMachines +// listKind VirtualMachineList -> PrefixedVirtualMachineList +// plural virtualmachines -> prefixedvirtualmachines +// singular virtualmachine -> prefixedvirtualmachine +// shortNames [vm vms] -> [xvm xvms] +func RenameCRD(rules *RewriteRules, obj []byte) ([]byte, error) { + crdName := gjson.GetBytes(obj, "metadata.name").String() + resource, group, found := strings.Cut(crdName, ".") + if !found { + return nil, fmt.Errorf("malformed CRD name: should be resourcetype.group, got %s", crdName) + } + + _, resourceRule := rules.ResourceRules(group, resource) + if resourceRule == nil { + return nil, nil + } + + newName := rules.RenameResource(resource) + "." + rules.RenameApiVersion(group) + obj, err := sjson.SetBytes(obj, "metadata.name", newName) + if err != nil { + return nil, err + } + + spec := gjson.GetBytes(obj, "spec") + newSpec, err := renameCRDSpec(rules, resourceRule, []byte(spec.Raw)) + if err != nil { + return nil, err + } + return sjson.SetRawBytes(obj, "spec", newSpec) +} + +func renameCRDSpec(rules *RewriteRules, resourceRule *ResourceRule, spec []byte) ([]byte, error) { + var err error + + spec, err = TransformString(spec, "group", func(crdSpecGroup string) string { + return rules.RenameApiVersion(crdSpecGroup) + }) + if err != nil { + return nil, err + } + + // Rename fields in the 'names' object. + names := []byte(gjson.GetBytes(spec, "names").Raw) + + if gjson.GetBytes(names, "categories").Exists() { + names, err = sjson.SetBytes(names, "categories", rules.RenameCategories(resourceRule.Categories)) + if err != nil { + return nil, err + } + } + if gjson.GetBytes(names, "kind").Exists() { + names, err = sjson.SetBytes(names, "kind", rules.RenameKind(resourceRule.Kind)) + if err != nil { + return nil, err + } + } + if gjson.GetBytes(names, "listKind").Exists() { + names, err = sjson.SetBytes(names, "listKind", rules.RenameKind(resourceRule.ListKind)) + if err != nil { + return nil, err + } + } + if gjson.GetBytes(names, "plural").Exists() { + names, err = sjson.SetBytes(names, "plural", rules.RenameResource(resourceRule.Plural)) + if err != nil { + return nil, err + } + } + if gjson.GetBytes(names, "singular").Exists() { + names, err = sjson.SetBytes(names, "singular", rules.RenameResource(resourceRule.Singular)) + if err != nil { + return nil, err + } + } + if gjson.GetBytes(names, "shortNames").Exists() { + names, err = sjson.SetBytes(names, "shortNames", rules.RenameShortNames(resourceRule.ShortNames)) + if err != nil { + return nil, err + } + } + + spec, err = sjson.SetRawBytes(spec, "names", names) + if err != nil { + return nil, err + } + + return spec, nil +} + +func RenameCRDPatch(rules *RewriteRules, resourceRule *ResourceRule, obj []byte) ([]byte, error) { + var err error + + obj, err = RenameMetadataPatch(rules, obj) + if err != nil { + return nil, fmt.Errorf("rename metadata patches for CRD: %w", err) + } + + isRenamed := false + newPatches, err := RewriteArray(obj, Root, func(singlePatch []byte) ([]byte, error) { + op := gjson.GetBytes(singlePatch, "op").String() + path := gjson.GetBytes(singlePatch, "path").String() + + if (op == "replace" || op == "add") && path == "/spec" { + isRenamed = true + value := []byte(gjson.GetBytes(singlePatch, "value").Raw) + newValue, err := renameCRDSpec(rules, resourceRule, value) + if err != nil { + return nil, err + } + return sjson.SetRawBytes(singlePatch, "value", newValue) + } + + return nil, nil + }) + + if !isRenamed { + return obj, nil + } + + return newPatches, nil +} diff --git a/images/kube-api-rewriter/pkg/rewriter/crd_test.go b/images/kube-api-rewriter/pkg/rewriter/crd_test.go new file mode 100644 index 0000000..ffdf20c --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/crd_test.go @@ -0,0 +1,336 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createRewriterForCRDTest() *RuleBasedRewriter { + apiGroupRules := map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"sr", "srs"}, + }, + "anotherresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + Plural: "anotherresources", + Singular: "anotherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"ar"}, + }, + }, + }, + "other.group.io": { + GroupRule: GroupRule{ + Group: "other.group.io", + Versions: []string{"v2alpha3"}, + PreferredVersion: "v2alpha3", + Renamed: "other.prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"or"}, + }, + }, + }, + } + + rwRules := &RewriteRules{ + KindPrefix: "Prefixed", // KV + ResourceTypePrefix: "prefixed", // kv + ShortNamePrefix: "p", + Categories: []string{"prefixed"}, + Rules: apiGroupRules, + } + + rwRules.Init() + return &RuleBasedRewriter{ + Rules: rwRules, + } +} + +// TestCRDRename - rename of a single CRD. +func TestCRDRename(t *testing.T) { + reqBody := `{ +"apiVersion": "apiextensions.k8s.io/v1", +"kind": "CustomResourceDefinition", +"metadata": { + "name":"someresources.original.group.io" +} +"spec": { + "group": "original.group.io", + "names": { + "kind": "SomeResource", + "listKind": "SomeResourceList", + "plural": "someresources", + "singular": "someresource", + "shortNames": ["sr"], + "categories": ["all"] + }, + "scope":"Namespaced", + "versions": {} +} +}` + rwr := createRewriterForCRDTest() + testCRDRules := rwr.Rules + + restored, err := RewriteCRDOrList(testCRDRules, []byte(reqBody), Rename) + if err != nil { + t.Fatalf("should rename CRD without error: %v", err) + } + if restored == nil { + t.Fatalf("should rename CRD: %v", err) + } + + groupRule, resRule := testCRDRules.KindRules("original.group.io", "SomeResource") + + tests := []struct { + path string + expected string + }{ + {"metadata.name", testCRDRules.RenameResource(resRule.Plural) + "." + groupRule.Renamed}, + {"spec.group", groupRule.Renamed}, + {"spec.names.kind", testCRDRules.RenameKind(resRule.Kind)}, + {"spec.names.listKind", testCRDRules.RenameKind(resRule.ListKind)}, + {"spec.names.plural", testCRDRules.RenameResource(resRule.Plural)}, + {"spec.names.singular", testCRDRules.RenameResource(resRule.Singular)}, + {"spec.names.shortNames", `["psr","psrs"]`}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(restored, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} + +// TestCRDPatch tests renaming /spec in a CRD patch. +func TestCRDPatch(t *testing.T) { + patches := `[{ "op": "add", "path": "/metadata/ownerReferences", "value": null }, +{ "op": "replace", "path": "/spec", "value": { +"group":"original.group.io", +"names":{"plural":"someresources","singular":"someresource","shortNames":["sr","srs"],"kind":"SomeResource","categories":["all"]}, +"scope":"Namespaced","versions":[{"name":"v1alpha1","schema":{}}] +} } +]` + patches = strings.ReplaceAll(patches, "\n", "") + + expect := `[{ "op": "add", "path": "/metadata/ownerReferences", "value": null }, +{ "op": "replace", "path": "/spec", "value": { +"group":"prefixed.resources.group.io", +"names":{"plural":"prefixedsomeresources","singular":"prefixedsomeresource","shortNames":["psr","psrs"],"kind":"PrefixedSomeResource","categories":["prefixed"]}, +"scope":"Namespaced","versions":[{"name":"v1alpha1","schema":{}}] +} } +]` + expect = strings.ReplaceAll(expect, "\n", "") + + rwr := createRewriterForCRDTest() + _, resRule := rwr.Rules.ResourceRules("original.group.io", "someresources") + require.NotNil(t, resRule, "should get resource rule for hardcoded group and resourceType") + + resBytes, err := RenameCRDPatch(rwr.Rules, resRule, []byte(patches)) + require.NoError(t, err, "should rename CRD patch") + + actual := string(resBytes) + require.Equal(t, expect, actual) +} + +// TestCRDRestore test restoring of a single CRD. +func TestCRDRestore(t *testing.T) { + crdHTTPRequest := `GET /apis/apiextensions.k8s.io/v1/customresourcedefinitions/someresources.original.group.io HTTP/1.1 +Host: 127.0.0.1 + +` + origGroup := "original.group.io" + crdPayload := `{ +"apiVersion": "apiextensions.k8s.io/v1", +"kind": "CustomResourceDefinition", +"metadata": { + "name":"prefixedsomeresources.prefixed.resources.group.io" +} +"spec": { + "group": "prefixed.resources.group.io", + "names": { + "kind": "PrefixedSomeResource", + "listKind": "PrefixedSomeResourceList", + "plural": "prefixedsomeresources", + "singular": "prefixedsomeresource", + "shortNames": ["psr"], + "categories": ["prefixed"] + }, + "scope":"Namespaced", + "versions": {} +} +}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(crdHTTPRequest))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createRewriterForCRDTest() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(crdPayload), Restore) // RewriteCRDOrList(crdPayload, []byte(reqBody), Restore, origGroup) + if err != nil { + t.Fatalf("should restore CRD without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore CRD: %v", err) + } + + resRule := rwr.Rules.Rules[origGroup].ResourceRules["someresources"] + + tests := []struct { + path string + expected string + }{ + {"metadata.name", resRule.Plural + "." + origGroup}, + {"spec.group", origGroup}, + {"spec.names.kind", resRule.Kind}, + {"spec.names.listKind", resRule.ListKind}, + {"spec.names.plural", resRule.Plural}, + {"spec.names.singular", resRule.Singular}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} + +func TestCRDPathRewrite(t *testing.T) { + tests := []struct { + name string + urlPath string + expected string + origGroup string + origResourceType string + }{ + { + "crd with rule", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/someresources.original.group.io", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/prefixedsomeresources.prefixed.resources.group.io", + "original.group.io", + "someresources", + }, + { + "crd watch by name", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?fieldSelector=metadata.name%3Dsomeresources.original.group.io&resourceVersion=0&watch=true", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?fieldSelector=metadata.name%3Dprefixedsomeresources.prefixed.resources.group.io&resourceVersion=0&watch=true", + "", + "", + }, + { + "unknown crd watch by name", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?fieldSelector=metadata.name%3Dresource.unknown.group.io&resourceVersion=0&watch=true", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions?fieldSelector=metadata.name%3Dresource.unknown.group.io&resourceVersion=0&watch=true", + "", + "", + }, + { + "crd without rule", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/unknown.group.io", + "", + "", + "", + }, + { + "crd list", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions", + "", + "", + "", + }, + { + "non crd apiextension", + "/apis/apiextensions.k8s.io/v1/unknown", + "", + "", + "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpReqHead := fmt.Sprintf(`GET %s HTTP/1.1`, tt.urlPath) + httpReq := httpReqHead + "\n" + "Host: 127.0.0.1\n\n" + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(httpReq))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createRewriterForCRDTest() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + + if tt.expected == "" { + require.Equal(t, tt.urlPath, targetReq.Path(), "should not rewrite api endpoint path") + return + } + + if tt.origGroup != "" { + require.Equal(t, tt.origGroup, targetReq.OrigGroup()) + } + + actual := targetReq.Path() + if targetReq.RawQuery() != "" { + actual += "?" + targetReq.RawQuery() + } + + require.Equal(t, tt.expected, actual, "should rewrite api endpoint path") + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/discovery.go b/images/kube-api-rewriter/pkg/rewriter/discovery.go new file mode 100644 index 0000000..0f2f515 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/discovery.go @@ -0,0 +1,574 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bytes" + "fmt" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// RewriteAPIGroupList restores groups and kinds in "groups" array in /apis/ response. +// +// Response example: +// +// { +// "kind": "APIGroupList", +// "apiVersion": "v1", +// "groups": [ +// { +// "name": "prefixed.resources.group.io", +// "versions": [ +// {"groupVersion":"prefixed.resources.group.io/v1","version":"v1"}, +// {"groupVersion":"prefixed.resources.group.io/v1beta1","version":"v1beta1"}, +// {"groupVersion":"prefixed.resources.group.io/v1alpha3","version":"v1alpha3"} +// ], +// "preferredVersion": { +// "groupVersion":"prefixed.resources.group.io/v1", +// "version":"v1" +// } +// } +// ] +// } +func RewriteAPIGroupList(rules *RewriteRules, obj []byte) ([]byte, error) { + return RewriteArray(obj, "groups", func(groupObj []byte) ([]byte, error) { + // Remove original groups to prevent duplicates if cluster have CRDs with original names. + groupName := gjson.GetBytes(groupObj, "name").String() + if rules.HasGroup(groupName) { + return nil, SkipItem + } + + groupObj, err := TransformString(groupObj, "name", func(name string) string { + return rules.RestoreApiVersion(name) + }) + if err != nil { + return nil, err + } + + groupObj, err = TransformString(groupObj, "preferredVersion.groupVersion", func(groupVersion string) string { + return rules.RestoreApiVersion(groupVersion) + }) + if err != nil { + return nil, err + } + + return RewriteArray(groupObj, "versions", func(versionObj []byte) ([]byte, error) { + return TransformString(versionObj, "groupVersion", func(groupVersion string) string { + return rules.RestoreApiVersion(groupVersion) + }) + }) + }) +} + +// RewriteAPIGroup restores apiGroup, kinds and versions in responses from renamed APIGroup query: +// /apis/renamed.resource.group.io +// +// This call returns all versions for renamed.resource.group.io. +// Rewriter should reduce versions for only available in original group +// To reduce further requests with specific versions. +// +// Example response with renamed group: +// { "kind":"APIGroup", +// +// "apiVersion":"v1", +// "name":"renamed.resource.group.io", +// "versions":[ +// {"groupVersion":"renamed.resource.group.io/v1","version":"v1"}, +// {"groupVersion":"renamed.resource.group.io/v1alpha1","version":"v1alpha1"} +// ], +// "preferredVersion": { +// "groupVersion":"renamed.resource.group.io/v1", +// "version":"v1"} +// } +// +// Restored response should be: +// { "kind":"APIGroup", +// +// "apiVersion":"v1", +// "name":"original.group.io", +// "versions":[ +// {"groupVersion":"original.group.io/v1","version":"v1"}, +// {"groupVersion":"original.group.io/v1alpha1","version":"v1alpha1"} +// ], +// "preferredVersion": { +// "groupVersion":"original.group.io/v1", +// "version":"v1"} +// } +func RewriteAPIGroup(rules *RewriteRules, obj []byte) ([]byte, error) { + groupName := gjson.GetBytes(obj, "name").String() + // Return as-is for group without rules. + if !rules.IsRenamedGroup(groupName) { + return obj, nil + } + obj, err := sjson.SetBytes(obj, "name", rules.RestoreApiVersion(groupName)) + if err != nil { + return nil, err + } + + obj, err = RewriteArray(obj, "versions", func(versionObj []byte) ([]byte, error) { + return TransformString(versionObj, "groupVersion", func(groupVersion string) string { + return rules.RestoreApiVersion(groupVersion) + }) + }) + if err != nil { + return nil, err + } + + return TransformString(obj, "preferredVersion.groupVersion", func(preferredGroupVersion string) string { + return rules.RestoreApiVersion(preferredGroupVersion) + }) +} + +// RewriteAPIResourceList rewrites server responses from /apis/GROUP/VERSION discovery requests. +// +// Example: +// +// Path rewrite: https://10.222.0.1:443/apis/original.group.io/v1 -> https://10.222.0.1:443/apis/prefixed.resources.group.io/v1 +// 1. Restore "groupVersion" field. +// 2. Restore items in "resources": +// 2.1. If name is a resource type: restore "name", "singularName", "kind", "shortNames", and "categories". +// 2.2. If name contains "/status" suffix: restore "name" and "kind" fields +// 2.3. If name contains "/scale" suffix: restore "name" field as a resource type +// +// Rewrite of response from /apis/prefixed.resources.group.io/v1: +// +// { +// "kind":"APIResourceList", +// "apiVersion":"v1", +// "groupVersion":"prefixed.resources.group.io/v1", --> Restore apiGroup, keep version: original.group.io/v1 +// "resources":[ +// { +// "name":"prefixedsomeresources", --> Restore resource type: someresources +// "singularName":"prefixedsomeresource", --> Restore singular: someresource +// "namespaced":true, +// "kind":"PrefixedSomeResource", --> restore kind: SomeResource +// "verbs":["delete","deletecollection","get","list","patch","create","update","watch"], +// "shortNames":["psr","psrs"], --> Restore shortNames: ["sr", "srs"] +// "categories":["prefixed"], --> Restore categories: ["all"] +// "storageVersionHash":"QUMxLW9gfYs=" +// },{ +// "name":"prefixedsomeresources/status", --> Restore resource type, keep suffix: someresources/status +// "singularName":"", +// "namespaced":true, +// "kind":"PrefixedSomeResource", --> Restore kind: SomeResource +// "verbs":["get","patch","update"] +// },{ +// "name":"prefixedsomeresources/scale", --> Restore resource type, keep suffix: someresources/status +// "singularName":"", +// "namespaced":true, +// "group":"autoscaling", +// "version":"v1", +// "kind":"Scale", +// "verbs":["get","patch","update"] +// }] +// } +// } +func RewriteAPIResourceList(rules *RewriteRules, obj []byte) ([]byte, error) { + // Check if groupVersion is renamed and save restored group. + // No rewrite if groupVersion has no rules. + groupVersion := gjson.GetBytes(obj, "groupVersion").String() + if !rules.IsRenamedGroup(groupVersion) { + return obj, nil + } + origGroup := rules.RestoreApiVersion(groupVersion) + obj, err := sjson.SetBytes(obj, "groupVersion", origGroup) + if err != nil { + return nil, err + } + + // Rewrite "resources" array. + return RewriteArray(obj, "resources", func(resource []byte) ([]byte, error) { + name := gjson.GetBytes(resource, "name").String() + origResourceType := rules.RestoreResource(name) + + // No rewrite if resource has no rules. + _, resourceRule := rules.ResourceRules(origGroup, origResourceType) + if resourceRule == nil { + return resource, nil + } + + resource, err = TransformString(resource, "name", func(name string) string { + return origResourceType + }) + if err != nil { + return nil, err + } + + resource, err = TransformString(resource, "kind", func(kind string) string { + return rules.RestoreKind(kind) + }) + if err != nil { + return nil, err + } + + resource, err = TransformString(resource, "singularName", func(singularName string) string { + return rules.RestoreResource(singularName) + }) + if err != nil { + return nil, err + } + + resource, err = TransformArrayOfStrings(resource, "shortNames", func(shortName string) string { + return rules.RestoreShortName(shortName) + }) + if err != nil { + return nil, err + } + + categories := gjson.GetBytes(resource, "categories") + if categories.Exists() { + restoredCategories := rules.RestoreCategories(resourceRule) + resource, err = sjson.SetBytes(resource, "categories", restoredCategories) + if err != nil { + return nil, err + } + } + + return resource, nil + }) +} + +// RewriteAPIGroupDiscoveryList restores renamed groups and resources in the aggregated +// discovery response (APIGroupDiscoveryList kind). +// +// Example of APIGroupDiscoveryList structure: +// +// { +// "kind": "APIGroupDiscoveryList", +// "apiVersion": "apidiscovery.k8s.io/v2beta1", +// "metadata": {}, +// "items": [ +// An array of APIGroupDiscovery objects ... +// { +// "metadata": { +// "name": "internal.virtualization.deckhouse.io", <-- should be renamed group +// "creationTimestamp": null +// }, +// "versions": [ +// APIVersionDiscovery, .. , APIVersionDiscovery +// ] +// }, ... +// ] +// +// NOTE: Can't use RewriteArray here, because one APIGroupDiscovery with renamed +// resource produces many APIGroupDiscovery objects with restored resource. + +func newSliceBytesBuilder() *sliceBytesBuilder { + return &sliceBytesBuilder{ + buf: bytes.NewBuffer([]byte("[")), + } +} + +type sliceBytesBuilder struct { + buf *bytes.Buffer + begin bool +} + +func (b *sliceBytesBuilder) WriteString(s string) { + if s == "" { + return + } + if b.begin { + b.buf.WriteString(",") + } + b.buf.WriteString(s) + b.begin = true +} + +func (b *sliceBytesBuilder) Write(bytes []byte) { + if len(bytes) == 0 { + return + } + if b.begin { + b.buf.WriteString(",") + } + b.buf.Write(bytes) + b.begin = true +} + +func (b *sliceBytesBuilder) Complete() *sliceBytesBuilder { + b.buf.WriteString("]") + return b +} + +func (b *sliceBytesBuilder) Bytes() []byte { + return b.buf.Bytes() +} + +func RewriteAPIGroupDiscoveryList(rules *RewriteRules, obj []byte) ([]byte, error) { + items := gjson.GetBytes(obj, "items").Array() + if len(items) == 0 { + return obj, nil + } + + rwrItems := newSliceBytesBuilder() + + for _, item := range items { + + itemBytes := []byte(item.Raw) + var err error + + groupName := gjson.GetBytes(itemBytes, "metadata.name").String() + + if !rules.IsRenamedGroup(groupName) { + // Remove duplicates if cluster have CRDs with original group names. + if rules.HasGroup(groupName) { + continue + } + + // No transform for non-renamed groups, add as-is. + rwrItems.Write(itemBytes) + continue + } + + newItems, err := RestoreAggregatedGroupDiscovery(rules, itemBytes) + if err != nil { + return nil, err + } + if newItems == nil { + rwrItems.Write(itemBytes) + } else { + // Replace renamed group with restored groups. + for _, newItem := range newItems { + rwrItems.Write(newItem) + } + } + } + + return sjson.SetRawBytes(obj, "items", rwrItems.Complete().Bytes()) +} + +// RestoreAggregatedGroupDiscovery returns an array of APIGroupDiscovery objects with restored resources. +// +// obj is an APIGroupDiscovery object with renamed resources: +// +// { +// "metadata": { +// "name": "internal.virtualization.deckhouse.io", <-- renamed group +// "creationTimestamp": null +// }, +// "versions": [ +// { // APIVersionDiscovery +// "version": "v1", +// "resources": [ APIResourceDiscovery{}, ..., APIResourceDiscovery{}] , +// "freshness": "Current" +// }, ... , more APIVersionDiscovery objects. +// ] +// } +// +// Renamed resources in one version may belong to different original groups, +// so this method indexes and restores all resources in APIResourceDiscovery +// and then produces APIGroupDiscovery for each restored group. +func RestoreAggregatedGroupDiscovery(rules *RewriteRules, obj []byte) ([][]byte, error) { + // restoredResources holds restored resources indexed by group and version to construct final APIGroupDiscovery items later. + // A APIGroupDiscovery "metadata" object field and a version item "version" field are not stored and will be reconstructed. + restoredResources := make(map[string]map[string][][]byte) + + // versionFreshness stores freshness values for versions + versionFreshness := make(map[string]string) + + versions := gjson.GetBytes(obj, "versions").Array() + if len(versions) == 0 { + return nil, nil + } + + for _, version := range versions { + versionBytes := []byte(version.Raw) + + versionName := gjson.GetBytes(versionBytes, "version").String() + if versionName == "" { + continue + } + + // Save freshness. + freshness := gjson.GetBytes(versionBytes, "freshness").String() + versionFreshness[versionName] = freshness + + // Loop over resources. + resources := gjson.GetBytes(versionBytes, "resources").Array() + if len(resources) == 0 { + continue + } + + for _, resource := range resources { + restoredGroup, restoredResource, err := RestoreAggregatedDiscoveryResource(rules, []byte(resource.Raw)) + if err != nil { + return nil, nil + } + + if _, ok := restoredResources[restoredGroup]; !ok { + restoredResources[restoredGroup] = make(map[string][][]byte) + } + if _, ok := restoredResources[restoredGroup][versionName]; !ok { + restoredResources[restoredGroup][versionName] = make([][]byte, 0) + } + restoredResources[restoredGroup][versionName] = append(restoredResources[restoredGroup][versionName], restoredResource) + } + } + + // Produce restored APIGroupDiscovery items from indexed APIResourceDiscovery. + restoredGroupList := make([][]byte, 0, len(restoredResources)) + var err error + for groupName, groupVersions := range restoredResources { + // Restore metadata for APIGroupDiscovery. + restoredGroupObj := []byte(fmt.Sprintf(`{"metadata":{"name":"%s", "creationTimestamp":null}}`, groupName)) + + // Construct an array of APIVersionDiscovery objects. + restoredVersions := newSliceBytesBuilder() + for versionName, versionResources := range groupVersions { + // Init restored APIVersionDiscovery object. + restoredVersionObj := []byte(fmt.Sprintf(`{"version":"%s"}`, versionName)) + + // Construct an array of APIResourceDiscovery objects. + { + + restoredVersionResources := newSliceBytesBuilder() + for _, resource := range versionResources { + restoredVersionResources.Write(resource) + } + // Set resources field. + restoredVersionObj, err = sjson.SetRawBytes(restoredVersionObj, "resources", restoredVersionResources.Complete().Bytes()) + if err != nil { + return nil, err + } + } + + // Append restored APIVersionDiscovery object. + restoredVersions.Write(restoredVersionObj) + } + restoredGroupObj, err := sjson.SetRawBytes(restoredGroupObj, "versions", restoredVersions.Complete().Bytes()) + if err != nil { + return nil, err + } + + restoredGroupList = append(restoredGroupList, restoredGroupObj) + } + + return restoredGroupList, nil +} + +// RestoreAggregatedDiscoveryResource restores fields in a renamed APIResourceDiscovery object. +// +// Example of the APIResourceDiscovery object: +// +// { +// "resource": "internalvirtualizationkubevirts", +// "responseKind": { +// "group": "internal.virtualization.deckhouse.io", +// "version": "v1", +// "kind": "InternalVirtualizationKubeVirt" +// }, +// "scope": "Namespaced", +// "singularResource": "internalvirtualizationkubevirt", +// "verbs": [ "delete", "deletecollection", "get", ... ], // Optional +// "categories": [ "intvirt" ], // Optional +// "subresources": [ // Optional +// { +// "subresource": "status", +// "responseKind": { +// "group": "internal.virtualization.deckhouse.io", +// "version": "v1", +// "kind": "InternalVirtualizationKubeVirt" +// }, +// "verbs": [ "get", "patch", "update" ] +// } +// ] +// } +func RestoreAggregatedDiscoveryResource(rules *RewriteRules, obj []byte) (string, []byte, error) { + var err error + + // Get resource plural. + resource := gjson.GetBytes(obj, "resource").String() + origResource := rules.RestoreResource(resource) + + groupRule, resRule := rules.GroupResourceRules(origResource) + + // Ignore resource without rules. + if resRule == nil { + return "", nil, err + } + + origGroup := groupRule.Group + + obj, err = sjson.SetBytes(obj, "resource", origResource) + if err != nil { + return "", nil, err + } + + // Reconstruct group and kind in responseKind field. + responseKind := gjson.GetBytes(obj, "responseKind") + if responseKind.IsObject() { + obj, err = sjson.SetBytes(obj, "responseKind.group", origGroup) + if err != nil { + return "", nil, err + } + obj, err = sjson.SetBytes(obj, "responseKind.kind", resRule.Kind) + if err != nil { + return "", nil, err + } + } + + singular := gjson.GetBytes(obj, "singularResource").String() + if singular != "" { + obj, err = sjson.SetBytes(obj, "singularResource", rules.RestoreResource(singular)) + if err != nil { + return "", nil, err + } + } + + shortNames := gjson.GetBytes(obj, "shortNames").Array() + if len(shortNames) > 0 { + strShortNames := make([]string, 0, len(shortNames)) + for _, shortName := range shortNames { + strShortNames = append(strShortNames, shortName.String()) + } + newShortNames := rules.RestoreShortNames(strShortNames) + obj, err = sjson.SetBytes(obj, "shortNames", newShortNames) + if err != nil { + return "", nil, err + } + } + + categories := gjson.GetBytes(obj, "categories") + if categories.Exists() { + restoredCategories := rules.RestoreCategories(resRule) + obj, err = sjson.SetBytes(obj, "categories", restoredCategories) + if err != nil { + return "", nil, err + } + } + + obj, err = RewriteArray(obj, "subresources", func(item []byte) ([]byte, error) { + // Reconstruct group and kind in responseKind field. + responseKind := gjson.GetBytes(item, "responseKind") + if responseKind.IsObject() { + item, err = sjson.SetBytes(item, "responseKind.group", origGroup) + if err != nil { + return nil, err + } + item, err = sjson.SetBytes(item, "responseKind.kind", resRule.Kind) + if err != nil { + return nil, err + } + } + return item, nil + }) + + return origGroup, obj, nil +} diff --git a/images/kube-api-rewriter/pkg/rewriter/discovery_test.go b/images/kube-api-rewriter/pkg/rewriter/discovery_test.go new file mode 100644 index 0000000..44063e6 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/discovery_test.go @@ -0,0 +1,606 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "fmt" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createRewriterForDiscoveryTest() *RuleBasedRewriter { + apiGroupRules := map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"sr", "srs"}, + }, + "anotherresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + Plural: "anotherresources", + Singular: "anotherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"ar"}, + }, + }, + }, + "other.group.io": { + GroupRule: GroupRule{ + Group: "other.group.io", + Versions: []string{"v2alpha3"}, + PreferredVersion: "v2alpha3", + Renamed: "other.prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"or"}, + }, + }, + }, + } + + webhookRules := map[string]WebhookRule{ + "/validate-prefixed-resources-group-io-v1-prefixedsomeresource": { + Path: "/validate-original-group-io-v1-someresource", + Group: "original.group.io", + Resource: "someresources", + }, + } + + rwRules := &RewriteRules{ + KindPrefix: "Prefixed", // KV + ResourceTypePrefix: "prefixed", // kv + ShortNamePrefix: "p", + Categories: []string{"prefixed"}, + Rules: apiGroupRules, + Webhooks: webhookRules, + } + rwRules.Init() + + return &RuleBasedRewriter{ + Rules: rwRules, + } +} + +func TestRewriteRequestAPIGroupList(t *testing.T) { + // Request APIGroupList. + request := `GET /apis HTTP/1.1 +Host: 127.0.0.1 + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(request))) + require.NoError(t, err, "should read hardcoded request") + + expectPath := "/apis" + + // Response body with renamed APIGroupList + apiGroupResponse := `{ + "kind": "APIGroupList", + "apiVersion": "v1", + "groups": [ + { + "name": "original.group.io", + "versions": [ + {"groupVersion":"original.group.io/v1", "version":"v1"}, + {"groupVersion":"original.group.io/v1alpha1", "version":"v1alpha1"} + ], + "preferredVersion": { + "groupVersion": "original.group.io/v1", + "version":"v1" + } + }, + { + "name": "prefixed.resources.group.io", + "versions": [ + {"groupVersion":"prefixed.resources.group.io/v1", "version":"v1"}, + {"groupVersion":"prefixed.resources.group.io/v1alpha1", "version":"v1alpha1"} + ], + "preferredVersion": { + "groupVersion": "prefixed.resources.group.io/v1", + "version":"v1" + } + }, + { + "name": "other.prefixed.resources.group.io", + "versions": [ + {"groupVersion":"other.prefixed.resources.group.io/v2alpha3", "version":"v2alpha3"} + ], + "preferredVersion": { + "groupVersion": "other.prefixed.resources.group.io/v2alpha3", + "version":"v2alpha3" + } + } + ] +}` + + // Client proxy mode. + rwr := createRewriterForDiscoveryTest() + + var targetReq *TargetRequest + + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.Equal(t, expectPath, targetReq.Path(), "should rewrite api endpoint path") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(apiGroupResponse), Restore) + if err != nil { + t.Fatalf("should rewrite body with renamed resources: %v", err) + } + + tests := []struct { + path string + expected string + }{ + // Check no prefixed groups left after rewrite. + {`groups.#(name=="prefixed.resource.group.io").name`, ""}, + // Should have only 1 group instance, no duplicates. + {`groups.#(name=="original.group.io")#|#`, "1"}, + {`groups.#(name=="original.group.io").name`, "original.group.io"}, + {`groups.#(name=="original.group.io").preferredVersion.groupVersion`, "original.group.io/v1"}, + // Should not add more versions than there are in response. + {`groups.#(name=="original.group.io").versions.#`, "2"}, + {`groups.#(name=="original.group.io").versions.#(version="v1").groupVersion`, "original.group.io/v1"}, + {`groups.#(name=="original.group.io").versions.#(version="v1alpha1").groupVersion`, "original.group.io/v1alpha1"}, + // Check other.group.io is restored. + {`groups.#(name=="other.group.io")#|#`, "1"}, + {`groups.#(name=="other.group.io").name`, "other.group.io"}, + {`groups.#(name=="other.group.io").preferredVersion.groupVersion`, "other.group.io/v2alpha3"}, + // Should not add more versions than there are in response. + {`groups.#(name=="other.group.io").versions.#`, "1"}, + {`groups.#(name=="other.group.io").versions.#(version="v2alpha3").groupVersion`, "other.group.io/v2alpha3"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got '%s', rewritten APIGroupList: %s", tt.path, tt.expected, actual, string(resultBytes)) + } + }) + } +} + +func TestRewriteRequestAPIGroup(t *testing.T) { + // Request APIResourcesList of original, non-renamed resources. + request := `GET /apis/original.group.io HTTP/1.1 +Host: 127.0.0.1 + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(request))) + require.NoError(t, err, "should read hardcoded request") + + expectPath := "/apis/prefixed.resources.group.io" + + // Response body with renamed APIResourcesList + apiGroupResponse := `{ + "kind": "APIGroup", + "apiVersion": "v1", + "name": "prefixed.resources.group.io", + "versions": [ + {"groupVersion":"prefixed.resources.group.io/v1", "version":"v1"}, + {"groupVersion":"prefixed.resources.group.io/v1alpha1", "version":"v1alpha1"} + ], + "preferredVersion": { + "groupVersion": "prefixed.resources.group.io/v1", + "version":"v1" + } +}` + + // Client proxy mode. + rwr := createRewriterForDiscoveryTest() + + var targetReq *TargetRequest + + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.Equal(t, expectPath, targetReq.Path(), "should rewrite api endpoint path") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(apiGroupResponse), Restore) + if err != nil { + t.Fatalf("should rewrite body with renamed resources: %v", err) + } + + groupRule, _ := rwr.Rules.GroupResourceRules("someresources") + require.NotNil(t, groupRule, "should get rule for hard-coded resource type someresources") + + tests := []struct { + path string + expected string + }{ + {"name", groupRule.Group}, + {"versions.#(version==\"v1\").groupVersion", groupRule.Group + "/v1"}, + {"versions.#(version==\"v1alpha1\").groupVersion", groupRule.Group + "/v1alpha1"}, + {"preferredVersion.groupVersion", groupRule.Group + "/v1"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got '%s', rewritten APIGroup: %s", tt.path, tt.expected, actual, string(resultBytes)) + } + }) + } +} + +func TestRewriteRequestAPIGroupUnknownGroup(t *testing.T) { + // Request APIGroup discovery for unknown group. + request := `GET /apis/unknown.group.io HTTP/1.1 +Host: 127.0.0.1 + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(request))) + require.NoError(t, err, "should read hardcoded request") + + apiGroupResponse := `{ + "kind": "APIGroup", + "apiVersion": "v1", + "name": "unknown.group.io", + "versions": [ + {"groupVersion":"unknown.group.io/v1beta1", "version":"v1beta1"}, + {"groupVersion":"unknown.group.io/v1alpha3", "version":"v1alpha3"} + ], + "preferredVersion": { + "groupVersion": "unknown.group.io/v1beta1", + "version":"v1beta1" + } +}` + + // Client proxy mode. + rwr := createRewriterForDiscoveryTest() + + var targetReq *TargetRequest + + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.Equal(t, req.URL.Path, targetReq.Path(), "should not rewrite api endpoint path") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(apiGroupResponse), Restore) + if err != nil { + t.Fatalf("should rewrite body with renamed resources: %v", err) + } + + require.Equal(t, apiGroupResponse, string(resultBytes), "should not rewrite ApiGroup for unknown group") +} + +func TestRewriteRequestAPIResourceList(t *testing.T) { + // Request APIResourcesList of original, non-renamed resources. + // Note: use non preferred version. + request := `GET /apis/original.group.io/v1alpha1 HTTP/1.1 +Host: 127.0.0.1 + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(request))) + require.NoError(t, err, "should read hardcoded request") + + expectPath := "/apis/prefixed.resources.group.io/v1alpha1" + + // Response body with renamed APIResourcesList + resourceListPayload := `{ + "kind": "APIResourceList", + "apiVersion": "v1", + "groupVersion": "prefixed.resources.group.io/v1alpha1", + "resources": [ + {"name":"prefixedsomeresources", + "singularName":"prefixedsomeresource", + "namespaced":true, + "kind":"PrefixedSomeResource", + "verbs":["delete","deletecollection","get","list","patch","create","update","watch"], + "shortNames":["psr","psrs"], + "categories":["prefixed"], + "storageVersionHash":"1qIJ90Mhvd8="}, + + {"name":"prefixedsomeresources/status", + "singularName":"", + "namespaced":true, + "kind":"PrefixedSomeResource", + "verbs":["get","patch","update"]}, + + {"name":"norulesresources", + "singularName":"norulesresource", + "namespaced":true, + "kind":"NoRulesResource", + "verbs":["delete","deletecollection","get","list","patch","create","update","watch"], + "shortNames":["nrr"], + "categories":["prefixed"], + "storageVersionHash":"Nwlto9QquX0="}, + + {"name":"norulesresources/status", + "singularName":"", + "namespaced":true, + "kind":"NoRulesResource", + "verbs":["get","patch","update"]} +]}` + + // Client proxy mode. + rwr := createRewriterForDiscoveryTest() + + var targetReq *TargetRequest + + targetReq = NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.Equal(t, expectPath, targetReq.Path(), "should rewrite api endpoint path") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(resourceListPayload), Restore) + if err != nil { + t.Fatalf("should rewrite body with renamed resources: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {"groupVersion", "original.group.io/v1alpha1"}, + {"resources.#(name==\"someresources\").name", "someresources"}, + {"resources.#(name==\"someresources\").kind", "SomeResource"}, + {"resources.#(name==\"someresources\").singularName", "someresource"}, + {"resources.#(name==\"someresources\").categories.0", "all"}, + {"resources.#(name==\"someresources\").shortNames.0", "sr"}, + {"resources.#(name==\"someresources\").shortNames.1", "srs"}, + {"resources.#(name==\"someresources/status\").name", "someresources/status"}, + {"resources.#(name==\"someresources/status\").kind", "SomeResource"}, + {"resources.#(name==\"someresources/status\").singularName", ""}, + // norulesresources should not be restored. + {"resources.#(name==\"norulesresources\").name", "norulesresources"}, + {"resources.#(name==\"norulesresources\").kind", "NoRulesResource"}, + {"resources.#(name==\"norulesresources\").singularName", "norulesresource"}, + {"resources.#(name==\"norulesresources\").categories.0", "prefixed"}, + {"resources.#(name==\"norulesresources\").shortNames.0", "nrr"}, + {"resources.#(name==\"norulesresources/status\").name", "norulesresources/status"}, + {"resources.#(name==\"norulesresources/status\").kind", "NoRulesResource"}, + {"resources.#(name==\"norulesresources/status\").singularName", ""}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got '%s', rewritten APIGroupDiscovery: %s", tt.path, tt.expected, actual, string(resultBytes)) + } + }) + } +} + +func TestRewriteRequestAPIGroupDiscoveryList(t *testing.T) { + // Request aggregated discovery as APIGroupDiscoveryList kind. + request := `GET /apis HTTP/1.1 +Host: 127.0.0.1 +Accept: application/json;g=apidiscovery.k8s.io;v=v2beta1;as=APIGroupDiscoveryList + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(request))) + require.NoError(t, err, "should read hardcoded request") + + // This group contains resources from 2 original groups: + // - someresources.original.group.io with v1 and v1alpha1 version + // - otherresources.other.group.io of v2alpha3 version + // Restored list should contain 2 APIGroupDiscovery. + renamedAPIGroupDiscovery := `{ + "metadata":{ + "name": "prefixed.resources.group.io", + "creationTimestamp": null + }, + "versions":[ + { "version": "v1", + "freshness": "Current", + "resources": [ + { "resource": "prefixedsomeresources", + "responseKind": {"group": "prefixed.resources.group.io", "version": "v1", "kind": "PrefixedSomeResource"}, + "scope": "Namespaced", + "singularResource": "prefixedsomeresource", + "shortNames": ["psr"], + "categories": ["prefixed"], + "verbs": ["create", "patch"], + "subresources": [ + { "subresource": "status", + "responseKind": {"group": "prefixed.resources.group.io", "version": "v1", "kind": "PrefixedSomeResource"}, + "verbs": ["get", "patch"] + } + ] + } + ] + }, + { "version": "v1alpha1", + "resources": [ + { "resource": "prefixedsomeresources", + "responseKind": {"group": "prefixed.resources.group.io", "version": "v1alpha1", "kind": "PrefixedSomeResource"}, + "scope": "Namespaced", + "singularResource": "prefixedsomeresource", + "verbs": ["create", "patch"], + "subresources": [ + { "subresource": "status", + "responseKind": {"group": "prefixed.resources.group.io", "version": "v1alpha1", "kind": "PrefixedSomeResource"}, + "verbs": ["get", "patch"] + } + ] + } + ] + } + ] +}` + renamedOtherAPIGroupDiscovery := `{ + "metadata":{ + "name": "other.prefixed.resources.group.io", + "creationTimestamp": null + }, + "versions":[ + { "version": "v2alpha3", + "resources": [ + { "resource": "prefixedotherresources", + "responseKind": {"group": "other.prefixed.resources.group.io", "version": "v1alpha1", "kind": "PrefixedOtherResource"}, + "scope": "Namespaced", + "singularResource": "prefixedotherresource", + "verbs": ["create", "patch"], + "subresources": [ + { "subresource": "status", + "responseKind": {"group": "other.prefixed.resources.group.io", "version": "v1alpha1", "kind": "PrefixedOtherResource"}, + "verbs": ["get", "patch"] + } + ] + } + ] + } + ] +}` + // This groups should not be rewritten. + appsAPIGroupDiscovery := `{ + "metadata": { + "name": "apps", + "creationTimestamp": null + }, + "versions": [ + {"version": "v1", + "freshness": "Current", + "resources": [ + {"resource": "deployments", + "responseKind": {"group": "", "version": "", "kind": "Deployment"}, + "scope": "Namespaced", + "singularResource": "deployment", + "verbs": ["create", "patch"] + } + ]} + ] +}` + // This groups should not be rewritten. + nonRewritableAPIGroupDiscovery := `{ + "metadata": { + "name": "custom.resources.io", + "creationTimestamp": null + }, + "versions": [ + {"version": "v1", + "freshness": "Current", + "resources": [ + {"resource": "somecustomresources", + "responseKind": {"group": "custom.resources.io", "version": "v1", "kind": "SomeCustomResource"}, + "scope": "Namespaced", + "singularResource": "somecustomresource", + "verbs": ["create", "patch"] + } + ]} + ] +}` + + // Response body with renamed APIGroupDiscoveryList + apiGroupDiscoveryListPayload := fmt.Sprintf(`{ + "kind": "APIGroupDiscoveryList", + "apiVersion": "apidiscovery.k8s.io/v2beta1", + "metadata": {}, + "items": [ %s ] +}`, strings.Join([]string{ + appsAPIGroupDiscovery, + renamedAPIGroupDiscovery, + renamedOtherAPIGroupDiscovery, + nonRewritableAPIGroupDiscovery, + }, ",")) + + // Initialize rewriter using hard-coded client http request. + rwr := createRewriterForDiscoveryTest() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(apiGroupDiscoveryListPayload), Restore) + if err != nil { + t.Fatalf("should rewrite body with renamed resources: %v", err) + } + + // Get rules for rewritable resource. + groupRule, resRule := rwr.Rules.GroupResourceRules("someresources") + require.NotNil(t, groupRule, "should get groupRule for hardcoded resourceType") + require.NotNil(t, resRule, "should get resourceRule for hardcoded resourceType") + + // Expect renamed groups present in the restored object. + { + expected := []string{ + "apps", + "original.group.io", + "other.group.io", + "custom.resources.io", + } + + groups := gjson.GetBytes(resultBytes, `items.#.metadata.name`).Array() + + actual := []string{} + for _, group := range groups { + actual = append(actual, group.String()) + } + + require.Equal(t, len(expected), len(groups), "restored object should have %d groups, got %d: %#v", len(expected), len(groups), actual) + for _, expect := range expected { + require.Contains(t, actual, expect, "restored object should have group %s, got %v", expect, actual) + } + } + + // Test renamed fields for someresources in original.group.io. + { + group := gjson.GetBytes(resultBytes, `items.#(metadata.name=="original.group.io")`) + groupRule, resRule := rwr.Rules.GroupResourceRules("someresources") + + require.NotNil(t, resRule, "should get rule for hard-coded resource type someresources") + + tests := []struct { + path string + expected string + }{ + {"versions.#(version==\"v1\").resources.0.resource", resRule.Plural}, + {"versions.#(version==\"v1\").resources.0.responseKind.group", groupRule.Group}, + {"versions.#(version==\"v1\").resources.0.responseKind.kind", resRule.Kind}, + {"versions.#(version==\"v1\").resources.0.singularResource", resRule.Singular}, + {"versions.#(version==\"v1\").resources.0.categories.0", resRule.Categories[0]}, + {"versions.#(version==\"v1\").resources.0.shortNames.0", resRule.ShortNames[0]}, + {"versions.#(version==\"v1\").resources.0.subresources.0.responseKind.group", groupRule.Group}, + {"versions.#(version==\"v1\").resources.0.subresources.0.responseKind.kind", resRule.Kind}, + } + + groupBytes := []byte(group.Raw) + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(groupBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got '%s', rewritten APIGroupDiscovery: %s", tt.path, tt.expected, actual, string(groupBytes)) + } + }) + } + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/events.go b/images/kube-api-rewriter/pkg/rewriter/events.go new file mode 100644 index 0000000..3de3894 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/events.go @@ -0,0 +1,52 @@ +/* +Copyright 2024 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 rewriter + +const ( + EventKind = "Event" + EventListKind = "EventList" +) + +// RewriteEventOrList rewrites a single Event resource or a list of Events in EventList. +// The only field need to rewrite is involvedObject: +// +// { +// "metadata": { "name": "...", "namespace": "...", "managedFields": [...] }, +// "involvedObject": { +// "kind": "SomeResource", +// "namespace": "name", +// "name": "ns", +// "uid": "a260fe4f-103a-41c6-996c-d29edb01fbbd", +// "apiVersion": "group.io/v1" +// }, +// "type": "...", +// "reason": "...", +// "message": "...", +// "source": { +// "component": "...", +// "host": "..." +// }, +// "reportingComponent": "...", +// "reportingInstance": "..." +// }, +func RewriteEventOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, EventListKind, func(singleObj []byte) ([]byte, error) { + return TransformObject(singleObj, "involvedObject", func(involvedObj []byte) ([]byte, error) { + return RewriteAPIVersionAndKind(rules, involvedObj, action) + }) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/events_test.go b/images/kube-api-rewriter/pkg/rewriter/events_test.go new file mode 100644 index 0000000..0574238 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/events_test.go @@ -0,0 +1,123 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestRewriteEvent(t *testing.T) { + eventReq := `POST /api/v1/namespaces/vm/events HTTP/1.1 +Host: 127.0.0.1 + +` + eventPayload := `{ + "kind": "Event", + "apiVersion": "v1", + "metadata": { + "name": "some-event-name", + "namespace": "vm", + }, + "involvedObject": { + "kind": "SomeResource", + "namespace": "vm", + "name": "some-vm-name", + "uid": "ad9f7357-f6b0-4679-8571-042c75ec53fb", + "apiVersion": "original.group.io/v1" + }, + "reason": "EventReason", + "message": "Event message for some-vm-name", + "source": { + "component": "some-component", + "host": "some-node" + }, + "count": 1000, + "type": "Warning", + "eventTime": null, + "reportingComponent": "some-component", + "reportingInstance": "some-node" +}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(eventReq + eventPayload))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriterForCore() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(eventPayload), Rename) + if err != nil { + t.Fatalf("should rename Error without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename Error: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`involvedObject.kind`, "PrefixedSomeResource"}, + {`involvedObject.apiVersion`, "prefixed.resources.group.io/v1"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + + // Restore. + resultBytes, err = rwr.RewriteJSONPayload(targetReq, []byte(eventPayload), Restore) + if err != nil { + t.Fatalf("should restore PVC without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore PVC: %v", err) + } + + tests = []struct { + path string + expected string + }{ + {`involvedObject.kind`, "SomeResource"}, + {`involvedObject.apiVersion`, "original.group.io/v1"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } + +} diff --git a/images/kube-api-rewriter/pkg/rewriter/gvk.go b/images/kube-api-rewriter/pkg/rewriter/gvk.go new file mode 100644 index 0000000..a318d6c --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/gvk.go @@ -0,0 +1,69 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +func RewriteAPIGroupAndKind(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteGVK(rules, obj, action, "apiGroup") +} + +func RewriteAPIVersionAndKind(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteGVK(rules, obj, action, "apiVersion") +} + +// RewriteGVK rewrites a "kind" field and a field with the group +// if there is the rule for these particular kind and group. +func RewriteGVK(rules *RewriteRules, obj []byte, action Action, gvFieldName string) ([]byte, error) { + kind := gjson.GetBytes(obj, "kind").String() + apiGroupVersion := gjson.GetBytes(obj, gvFieldName).String() + + rwrApiVersion := "" + rwrKind := "" + if action == Rename { + // Rename if there is a rule for kind and group + _, resourceRule := rules.KindRules(apiGroupVersion, kind) + if resourceRule == nil { + return obj, nil + } + rwrApiVersion = rules.RenameApiVersion(apiGroupVersion) + rwrKind = rules.RenameKind(kind) + } + if action == Restore { + // Restore if group is renamed and a rule can be found + // for restored kind and group. + if !rules.IsRenamedGroup(apiGroupVersion) { + return obj, nil + } + rwrApiVersion = rules.RestoreApiVersion(apiGroupVersion) + rwrKind = rules.RestoreKind(kind) + _, resourceRule := rules.KindRules(rwrApiVersion, rwrKind) + if resourceRule == nil { + return obj, nil + } + } + + obj, err := sjson.SetBytes(obj, "kind", rwrKind) + if err != nil { + return nil, err + } + + return sjson.SetBytes(obj, gvFieldName, rwrApiVersion) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/indexer/map_indexer.go b/images/kube-api-rewriter/pkg/rewriter/indexer/map_indexer.go new file mode 100644 index 0000000..6e2faaa --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/indexer/map_indexer.go @@ -0,0 +1,58 @@ +/* +Copyright 2024 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 indexer + +type MapIndexer struct { + idx map[string]string + reverse map[string]string +} + +func NewMapIndexer() *MapIndexer { + return &MapIndexer{ + idx: make(map[string]string), + reverse: make(map[string]string), + } +} + +func (m *MapIndexer) AddPair(original, renamed string) { + m.idx[original] = renamed + m.reverse[renamed] = original +} + +func (m *MapIndexer) Rename(original string) string { + if renamed, ok := m.idx[original]; ok { + return renamed + } + return original +} + +func (m *MapIndexer) Restore(renamed string) string { + if original, ok := m.reverse[renamed]; ok { + return original + } + return renamed +} + +func (m *MapIndexer) IsOriginal(original string) bool { + _, ok := m.idx[original] + return ok +} + +func (m *MapIndexer) IsRenamed(original string) bool { + _, ok := m.reverse[original] + return ok +} diff --git a/images/kube-api-rewriter/pkg/rewriter/list.go b/images/kube-api-rewriter/pkg/rewriter/list.go new file mode 100644 index 0000000..129dab5 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/list.go @@ -0,0 +1,101 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bytes" + "errors" + "strings" + + "github.com/tidwall/gjson" +) + +// TODO merge this file into transformers.go + +// RewriteResourceOrList is a helper to transform a single resource or a list of resources. +func RewriteResourceOrList(payload []byte, listKind string, transformFn func(singleObj []byte) ([]byte, error)) ([]byte, error) { + kind := gjson.GetBytes(payload, "kind").String() + + // Not a list, transform a single resource. + if kind != listKind { + return transformFn(payload) + } + + return RewriteArray(payload, "items", transformFn) +} + +// RewriteResourceOrList2 is a helper to transform a single resource or a list of resources. +func RewriteResourceOrList2(payload []byte, transformFn func(singleObj []byte) ([]byte, error)) ([]byte, error) { + kind := gjson.GetBytes(payload, "kind").String() + if !strings.HasSuffix(kind, "List") { + return transformFn(payload) + } + return RewriteArray(payload, "items", transformFn) +} + +// SkipItem may be used by the transformFn to indicate that the item should be skipped from the result. +var SkipItem = errors.New("remove item from the result") + +// RewriteArray gets array by path and transforms each item using transformFn. +// Use Root path to transform object itself. +// transformFn contract: +// return obj, nil -> obj is considered a replacement for the element. +// return nil, nil -> no transformation, element is added as-is. +// return any, SkipItem -> no transformation and no adding to the result. +// return any, err -> stop transformation, return error. +func RewriteArray(obj []byte, arrayPath string, transformFn func(item []byte) ([]byte, error)) ([]byte, error) { + // Transform each item in list. Put back original items if transformFn returns nil bytes. + items := GetBytes(obj, arrayPath).Array() + if len(items) == 0 { + return obj, nil + } + + var rwrItems bytes.Buffer + rwrItems.Grow(len(obj)) + // Start array + rwrItems.WriteString(`[`) + + first := true + for _, item := range items { + + rwrItem, err := transformFn([]byte(item.Raw)) + if err != nil { + if errors.Is(err, SkipItem) { + continue + } + return nil, err + } + + // Prepend a comma for all elements except the first one. + if first { + first = false + } else { + rwrItems.WriteString(`,`) + } + + // Put original item back to allow transformFn returns nil. + if rwrItem == nil { + rwrItem = []byte(item.Raw) + } + + rwrItems.Write(rwrItem) + } + + // Close array + rwrItems.WriteString(`]`) + return SetRawBytes(obj, arrayPath, rwrItems.Bytes()) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/load.go b/images/kube-api-rewriter/pkg/rewriter/load.go new file mode 100644 index 0000000..f44514a --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/load.go @@ -0,0 +1,38 @@ +/* +Copyright 2024 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 rewriter + +import ( + "os" + + "sigs.k8s.io/yaml" +) + +func LoadRules(filename string) (*RewriteRules, error) { + data, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + var rules = new(RewriteRules) + err = yaml.Unmarshal(data, rules) + if err != nil { + return nil, err + } + + return rules, nil +} diff --git a/images/kube-api-rewriter/pkg/rewriter/map.go b/images/kube-api-rewriter/pkg/rewriter/map.go new file mode 100644 index 0000000..83d51db --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/map.go @@ -0,0 +1,39 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// TODO merge this file into transformers.go + +// RewriteMapStringString transforms map[string]string value addressed by path. +func RewriteMapStringString(obj []byte, mapPath string, transformFn func(k, v string) (string, string)) ([]byte, error) { + m := gjson.GetBytes(obj, mapPath).Map() + if len(m) == 0 { + return obj, nil + } + newMap := make(map[string]string, len(m)) + for k, v := range m { + newK, newV := transformFn(k, v.String()) + newMap[newK] = newV + } + + return sjson.SetBytes(obj, mapPath, newMap) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/metadata.go b/images/kube-api-rewriter/pkg/rewriter/metadata.go new file mode 100644 index 0000000..8f6fa59 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/metadata.go @@ -0,0 +1,144 @@ +/* +Copyright 2024 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 rewriter + +import ( + "strings" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +func RewriteMetadata(rules *RewriteRules, metadataObj []byte, action Action) ([]byte, error) { + metadataObj, err := RewriteLabelsMap(rules, metadataObj, "labels", action) + if err != nil { + return nil, err + } + metadataObj, err = RewriteAnnotationsMap(rules, metadataObj, "annotations", action) + if err != nil { + return nil, err + } + metadataObj, err = RewriteFinalizers(rules, metadataObj, "finalizers", action) + if err != nil { + return nil, err + } + return RewriteOwnerReferences(rules, metadataObj, "ownerReferences", action) +} + +// RenameMetadataPatch transforms known metadata fields in patches. +// Example: +// - merge patch on metadata: +// {"metadata": { "labels": {"kubevirt.io/schedulable": "false", "cpumanager": "false"}, "annotations": {"kubevirt.io/heartbeat": "2024-06-07T23:27:53Z"}}} +// - JSON patch on metadata: +// [{"op":"test", "path":"/metadata/labels", "value":{"label":"value"}}, +// +// {"op":"replace", "path":"/metadata/labels", "value":{"label":"newValue"}}] +func RenameMetadataPatch(rules *RewriteRules, patch []byte) ([]byte, error) { + return TransformPatch(patch, + func(mergePatch []byte) ([]byte, error) { + return TransformObject(mergePatch, "metadata", func(metadataObj []byte) ([]byte, error) { + return RewriteMetadata(rules, metadataObj, Rename) + }) + }, + func(jsonPatch []byte) ([]byte, error) { + path := gjson.GetBytes(jsonPatch, "path").String() + switch path { + case "/metadata/labels": + return RewriteLabelsMap(rules, jsonPatch, "value", Rename) + case "/metadata/annotations": + return RewriteAnnotationsMap(rules, jsonPatch, "value", Rename) + case "/metadata/finalizers": + return RewriteFinalizers(rules, jsonPatch, "value", Rename) + case "/metadata/ownerReferences": + return RewriteOwnerReferences(rules, jsonPatch, "value", Rename) + case "/metadata": + return TransformObject(jsonPatch, "value", func(metadataObj []byte) ([]byte, error) { + return RewriteMetadata(rules, metadataObj, Rename) + }) + } + + encLabel, found := strings.CutPrefix(path, "/metadata/labels/") + if found { + label := decodeJSONPatchPath(encLabel) + rwrLabel := rules.LabelsRewriter().Rewrite(label, Rename) + if label != rwrLabel { + return sjson.SetBytes(jsonPatch, "path", "/metadata/labels/"+encodeJSONPatchPath(rwrLabel)) + } + } + + encAnno, found := strings.CutPrefix(path, "/metadata/annotations/") + if found { + anno := decodeJSONPatchPath(encAnno) + rwrAnno := rules.AnnotationsRewriter().Rewrite(anno, Rename) + if anno != rwrAnno { + return sjson.SetBytes(jsonPatch, "path", "/metadata/annotations/"+encodeJSONPatchPath(rwrAnno)) + } + } + + encFin, found := strings.CutPrefix(path, "/metadata/finalizers/") + if found { + fin := decodeJSONPatchPath(encFin) + rwrFin := rules.FinalizersRewriter().Rewrite(fin, Rename) + if fin != rwrFin { + return sjson.SetBytes(jsonPatch, "path", "/metadata/finalizers/"+encodeJSONPatchPath(rwrFin)) + } + } + + return jsonPatch, nil + }) +} + +func RewriteLabelsMap(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return RewriteMapStringString(obj, path, func(k, v string) (string, string) { + return rules.LabelsRewriter().RewriteNameValue(k, v, action) + }) +} + +func RewriteAnnotationsMap(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return RewriteMapStringString(obj, path, func(k, v string) (string, string) { + return rules.AnnotationsRewriter().RewriteNameValue(k, v, action) + }) +} + +func RewriteFinalizers(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return TransformArrayOfStrings(obj, path, func(finalizer string) string { + return rules.FinalizersRewriter().Rewrite(finalizer, action) + }) +} + +const ( + tildeChar = "~" + tildePlaceholder = "~0" + slashChar = "/" + slashPlaceholder = "~1" +) + +// decodeJSONPatchPath restores ~ and / from ~0 and ~1. +// See https://jsonpatch.com/#json-pointer +func decodeJSONPatchPath(path string) string { + // Restore / first to prevent tilde doubling. + res := strings.Replace(path, slashPlaceholder, slashChar, -1) + return strings.Replace(res, tildePlaceholder, tildeChar, -1) +} + +// encodeJSONPatchPath replaces ~ and / to ~0 and ~1. +// See https://jsonpatch.com/#json-pointer +func encodeJSONPatchPath(path string) string { + // Replace ~ first to prevent tilde doubling. + res := strings.Replace(path, tildeChar, tildePlaceholder, -1) + return strings.Replace(res, slashChar, slashPlaceholder, -1) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/path.go b/images/kube-api-rewriter/pkg/rewriter/path.go new file mode 100644 index 0000000..712d208 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/path.go @@ -0,0 +1,191 @@ +/* +Copyright 2024 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 rewriter + +// RewritePath return rewritten TargetPath along with original group and resource type. +// TODO: this rewriter is not conform to S in SOLID. Should split to ParseAPIEndpoint and RewriteAPIEndpoint. +//func (rw *RuleBasedRewriter) RewritePath(urlPath string) (*TargetRequest, error) { +// // Is it a webhook? +// if webhookRule, ok := rw.Rules.Webhooks[urlPath]; ok { +// return &TargetRequest{ +// Webhook: &webhookRule, +// }, nil +// } +// +// // Is it an API request? +// if strings.HasPrefix(urlPath, "/apis/") || urlPath == "/apis" { +// // TODO refactor RewriteAPIPath to produce a TargetPath, not an array in PathItems. +// cleanedPath := strings.Trim(urlPath, "/") +// pathItems := strings.Split(cleanedPath, "/") +// +// // First, try to rewrite CRD request. +// res := RewriteCRDPath(pathItems, rw.Rules) +// if res != nil { +// return res, nil +// } +// // Next, rewrite usual request. +// res, err := RewriteAPIsPath(pathItems, rw.Rules) +// if err != nil { +// return nil, err +// } +// if res == nil { +// // e.g. no rewrite rule find. +// return nil, nil +// } +// if len(res.PathItems) > 0 { +// res.TargetPath = "/" + path.Join(res.PathItems...) +// } +// return res, nil +// } +// +// if strings.HasPrefix(urlPath, "/api/") || urlPath == "/api" { +// return &TargetRequest{ +// IsCoreAPI: true, +// }, nil +// } +// +// return nil, nil +//} + +// Constants with indices of API endpoints portions. +// Request cluster scoped resource: +// - /apis/GROUP/VERSION/RESOURCETYPE/NAME/SUBRESOURCE +// | | | | +// APISIdx | | | +// GroupIDx | | +// VersionIDx ---+ | +// ClusterResourceIdx ---+ + +// +// Request namespaced resource: +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE +// | | | +// NamespacesIdx --------+ | | +// NamespaceIdx --------------------+ | +// NamespacedResourceIdx----------------------+ +// +// Request CRD: +// - /apis/apiextensions.k8s.io/v1/customresourcedefinitions/RESOURCETYPE.GROUP +// | | | +// GroupIdx | | +// ClusterResourceIdx -------------+ | +// CRDNameIdx -----------------------------------------------+ + +//const ( +// APISIdx = 0 +// GroupIdx = 1 +// VersionIdx = 2 +// NamespacesIdx = 3 +// NamespaceIdx = 4 +// ClusterResourceIdx = 3 +// NamespacedResourceIdx = 5 +//) + +// RewriteAPIsPath rewrites GROUP and RESOURCETYPE in these API calls: +// - /apis/GROUP +// - /apis/GROUP/VERSION +// - /apis/GROUP/VERSION/RESOURCETYPE +// - /apis/GROUP/VERSION/RESOURCETYPE/NAME +// - /apis/GROUP/VERSION/RESOURCETYPE/NAME/SUBRESOURCE +// +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME +// - /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE +//func RewriteAPIsPath(pathItems []string, rules *RewriteRules) (*TargetRequest, error) { +// if len(pathItems) == 0 { +// return nil, nil +// } +// +// res := &TargetRequest{ +// PathItems: make([]string, 0, len(pathItems)), +// } +// +// if len(pathItems) == 1 { +// if pathItems[APISIdx] == "apis" { +// // Do not rewrite URL, but rewrite response later. +// res.PathItems = append(res.PathItems, pathItems[APISIdx]) +// return res, nil +// } +// // The single path item should be "apis". +// return nil, nil +// } +// +// res.PathItems = append(res.PathItems, pathItems[APISIdx]) +// +// // Check if the GROUP portion match Rules. +// apiGroupName := "" +// apiGroupMatch := false +// group := pathItems[GroupIdx] +// for groupName, apiGroupRule := range rules.Rules { +// if apiGroupRule.GroupRule.Group == group { +// res.OrigGroup = group +// res.PathItems = append(res.PathItems, rules.RenamedGroup) +// apiGroupName = groupName +// apiGroupMatch = true +// break +// } +// } +// +// if !apiGroupMatch { +// return nil, nil +// } +// // Stop if GROUP is the last item in path. +// if len(pathItems) <= GroupIdx+1 { +// return res, nil +// } +// +// // Add VERSION portion. +// res.PathItems = append(res.PathItems, pathItems[VersionIdx]) +// // Stop if VERSION is the last item in path. +// if len(pathItems) <= VersionIdx+1 { +// return res, nil +// } +// +// // Check is namespaced resource is requested. +// resourceTypeIdx := ClusterResourceIdx +// if pathItems[NamespacesIdx] == "namespaces" { +// res.PathItems = append(res.PathItems, pathItems[NamespacesIdx]) +// res.PathItems = append(res.PathItems, pathItems[NamespaceIdx]) +// resourceTypeIdx = NamespacedResourceIdx +// } +// +// // Check if the RESOURCETYPE portion match Rules. +// resourceType := pathItems[resourceTypeIdx] +// resourceTypeMatched := true +// for _, rule := range rules.Rules[apiGroupName].ResourceRules { +// if rule.Plural == resourceType { +// res.OrigResourceType = resourceType +// res.PathItems = append(res.PathItems, rules.RenameResource(rule.Plural)) +// resourceTypeMatched = true +// break +// } +// } +// if !resourceTypeMatched { +// return nil, nil +// } +// // Return if RESOURCETYPE is the last item in path. +// if len(pathItems) == resourceTypeIdx+1 { +// return res, nil +// } +// +// // Copy remaining items: NAME and SUBRESOURCE. +// for i := resourceTypeIdx + 1; i < len(pathItems); i++ { +// res.PathItems = append(res.PathItems, pathItems[i]) +// } +// +// return res, nil +//} diff --git a/images/kube-api-rewriter/pkg/rewriter/policy.go b/images/kube-api-rewriter/pkg/rewriter/policy.go new file mode 100644 index 0000000..60e301f --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/policy.go @@ -0,0 +1,28 @@ +/* +Copyright 2024 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 rewriter + +const ( + PodDisruptionBudgetKind = "PodDisruptionBudget" + PodDisruptionBudgetListKind = "PodDisruptionBudgetList" +) + +func RewritePDBOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + return RewriteResourceOrList(obj, PodDisruptionBudgetListKind, func(singleObj []byte) ([]byte, error) { + return RewriteLabelsMap(rules, singleObj, "spec.selector.matchLabels", action) + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/prefixed_name_rewriter.go b/images/kube-api-rewriter/pkg/rewriter/prefixed_name_rewriter.go new file mode 100644 index 0000000..26246ef --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/prefixed_name_rewriter.go @@ -0,0 +1,288 @@ +/* +Copyright 2024 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 rewriter + +import "strings" + +const PreservedPrefix = "preserved-original-" + +type PrefixedNameRewriter struct { + namesRenameIdx map[string]string + namesRestoreIdx map[string]string + prefixRenameIdx map[string]string + prefixRestoreIdx map[string]string +} + +func NewPrefixedNameRewriter(replaceRules MetadataReplace) *PrefixedNameRewriter { + return &PrefixedNameRewriter{ + namesRenameIdx: indexRules(replaceRules.Names), + namesRestoreIdx: indexRulesReverse(replaceRules.Names), + prefixRenameIdx: indexRules(replaceRules.Prefixes), + prefixRestoreIdx: indexRulesReverse(replaceRules.Prefixes), + } +} + +func (p *PrefixedNameRewriter) Rewrite(name string, action Action) string { + switch action { + case Rename: + name, _ = p.rename(name, "") + case Restore: + name, _ = p.restore(name, "") + } + return name +} + +func (p *PrefixedNameRewriter) RewriteNameValue(name, value string, action Action) (string, string) { + switch action { + case Rename: + return p.rename(name, value) + case Restore: + return p.restore(name, value) + } + return name, value +} + +func (p *PrefixedNameRewriter) RewriteNameValues(name string, values []string, action Action) (string, []string) { + if len(values) == 0 { + return p.Rewrite(name, action), values + } + switch action { + case Rename: + return p.rewriteNameValues(name, values, p.rename) + case Restore: + return p.rewriteNameValues(name, values, p.restore) + } + return name, values +} + +func (p *PrefixedNameRewriter) RewriteSlice(names []string, action Action) []string { + switch action { + case Rename: + return p.rewriteSlice(names, p.rename) + case Restore: + return p.rewriteSlice(names, p.restore) + } + return names +} + +func (p *PrefixedNameRewriter) RewriteMap(names map[string]string, action Action) map[string]string { + switch action { + case Rename: + return p.rewriteMap(names, p.rename) + case Restore: + return p.rewriteMap(names, p.restore) + } + return names +} + +func (p *PrefixedNameRewriter) Rename(name, value string) (string, string) { + return p.rename(name, value) +} + +func (p *PrefixedNameRewriter) Restore(name, value string) (string, string) { + return p.restore(name, value) +} + +func (p *PrefixedNameRewriter) RenameSlice(names []string) []string { + return p.rewriteSlice(names, p.rename) +} + +func (p *PrefixedNameRewriter) RestoreSlice(names []string) []string { + return p.rewriteSlice(names, p.restore) +} + +func (p *PrefixedNameRewriter) RenameMap(names map[string]string) map[string]string { + return p.rewriteMap(names, p.rename) +} + +func (p *PrefixedNameRewriter) RestoreMap(names map[string]string) map[string]string { + return p.rewriteMap(names, p.restore) +} + +// rewriteNameValues rewrite name and values, e.g. for matchExpressions. +// Method uses all rules to detect a new name, first matching rule is applied. +// Values may be rewritten partially depending on specified name-value rules. +func (p *PrefixedNameRewriter) rewriteNameValues(name string, values []string, fn func(string, string) (string, string)) (string, []string) { + rwrName := name + rwrValues := make([]string, 0, len(values)) + + for _, value := range values { + n, v := fn(name, value) + // Set new name only for the first matching rule. + if n != name && rwrName == name { + rwrName = n + } + rwrValues = append(rwrValues, v) + } + + return rwrName, rwrValues +} + +func (p *PrefixedNameRewriter) rewriteMap(names map[string]string, fn func(string, string) (string, string)) map[string]string { + if names == nil { + return nil + } + result := make(map[string]string) + for name, value := range names { + rwrName, rwrValue := fn(name, value) + result[rwrName] = rwrValue + } + return result +} + +// rewriteSlice do not rewrite values, only names. +func (p *PrefixedNameRewriter) rewriteSlice(names []string, fn func(string, string) (string, string)) []string { + if names == nil { + return nil + } + result := make([]string, 0, len(names)) + for _, name := range names { + rwrName, _ := fn(name, "") + result = append(result, rwrName) + } + return result +} + +// rename rewrites original names and values. If label was preserved, rewrite it to original state. +func (p *PrefixedNameRewriter) rename(name, value string) (string, string) { + if p.isPreserved(name) { + return p.restorePreservedName(name), value + } + + // First try to find name and value. + if value != "" { + idxKey := joinKV(name, value) + if renamedIdxValue, ok := p.namesRenameIdx[idxKey]; ok { + return splitKV(renamedIdxValue) + } + } + // No exact rule for name and value, try to find exact name match. + if renamed, ok := p.namesRenameIdx[name]; ok { + return renamed, value + } + // No exact name, find prefix. + prefix, remainder, found := strings.Cut(name, "/") + if !found { + return name, value + } + if renamedPrefix, ok := p.prefixRenameIdx[prefix]; ok { + return renamedPrefix + "/" + remainder, value + } + return name, value +} + +// restore rewrites renamed names and values to their original state. +// If name is already original, preserve it with prefix, to make it unknown for client but keep in place for UPDATE/PATCH operations. +func (p *PrefixedNameRewriter) restore(name, value string) (string, string) { + if p.isOriginal(name, value) { + return p.preserveName(name), value + } + + // First try to find name and value. + if value != "" { + idxKey := joinKV(name, value) + if restoredIdxValue, ok := p.namesRestoreIdx[idxKey]; ok { + return splitKV(restoredIdxValue) + } + } + // No exact rule for name and value, try to find exact name match. + if restored, ok := p.namesRestoreIdx[name]; ok { + return restored, value + } + // No exact name, find prefix. + prefix, remainder, found := strings.Cut(name, "/") + if !found { + return name, value + } + if restoredPrefix, ok := p.prefixRestoreIdx[prefix]; ok { + return restoredPrefix + "/" + remainder, value + } + return name, value +} + +// isOriginal returns true if label should be renamed. +func (p *PrefixedNameRewriter) isOriginal(name, value string) bool { + if value != "" { + // Label is "original" if there is rule for renaming name and value. + idxKey := joinKV(name, value) + if _, ok := p.namesRenameIdx[idxKey]; ok { + return true + } + } + + // Try to find rule for exact name match. + if _, ok := p.namesRenameIdx[name]; ok { + return true + } + // No exact name, find rule for prefix. + prefix, _, found := strings.Cut(name, "/") + if !found { + // Label is only a name, but no rule for name found, so it is not "original". + return false + } + if _, ok := p.prefixRenameIdx[prefix]; ok { + return true + } + return false +} + +func (p *PrefixedNameRewriter) isPreserved(name string) bool { + return strings.HasPrefix(name, PreservedPrefix) +} + +func (p *PrefixedNameRewriter) preserveName(name string) string { + return PreservedPrefix + name +} + +func (p *PrefixedNameRewriter) restorePreservedName(name string) string { + return strings.TrimPrefix(name, PreservedPrefix) +} + +func indexRules(rules []MetadataReplaceRule) map[string]string { + idx := make(map[string]string, len(rules)) + for _, rule := range rules { + if rule.OriginalValue != "" && rule.RenamedValue != "" { + idxKey := joinKV(rule.Original, rule.OriginalValue) + idx[idxKey] = rule.Renamed + "=" + rule.RenamedValue + continue + } + idx[rule.Original] = rule.Renamed + } + return idx +} + +func indexRulesReverse(rules []MetadataReplaceRule) map[string]string { + idx := make(map[string]string, len(rules)) + for _, rule := range rules { + if rule.OriginalValue != "" && rule.RenamedValue != "" { + idxKey := joinKV(rule.Renamed, rule.RenamedValue) + idx[idxKey] = rule.Original + "=" + rule.OriginalValue + continue + } + idx[rule.Renamed] = rule.Original + } + return idx +} + +func joinKV(name, value string) string { + return name + "=" + value +} + +func splitKV(idxValue string) (name, value string) { + name, value, _ = strings.Cut(idxValue, "=") + return +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rbac.go b/images/kube-api-rewriter/pkg/rewriter/rbac.go new file mode 100644 index 0000000..004d166 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rbac.go @@ -0,0 +1,159 @@ +/* +Copyright 2024 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 rewriter + +const ( + ClusterRoleKind = "ClusterRole" + ClusterRoleListKind = "ClusterRoleList" + RoleKind = "Role" + RoleListKind = "RoleList" + RoleBindingKind = "RoleBinding" + RoleBindingListKind = "RoleBindingList" + ControllerRevisionKind = "ControllerRevision" + ControllerRevisionListKind = "ControllerRevisionList" + ClusterRoleBindingKind = "ClusterRoleBinding" + ClusterRoleBindingListKind = "ClusterRoleBindingList" + APIServiceKind = "APIService" + APIServiceListKind = "APIServiceList" +) + +func RewriteClusterRoleOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, ClusterRoleListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "rules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + } + return RewriteResourceOrList(obj, ClusterRoleListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "rules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) +} + +func RewriteRoleOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + if action == Rename { + return RewriteResourceOrList(obj, RoleListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "rules", func(item []byte) ([]byte, error) { + return RenameResourceRule(rules, item) + }) + }) + } + return RewriteResourceOrList(obj, RoleListKind, func(singleObj []byte) ([]byte, error) { + return RewriteArray(singleObj, "rules", func(item []byte) ([]byte, error) { + return RestoreResourceRule(rules, item) + }) + }) +} + +// RenameResourceRule renames apiGroups and resources in a single rule. +// Rule examples: +// - apiGroups: +// - original.group.io +// resources: +// - '*' +// verbs: +// - '*' +// - apiGroups: +// - original.group.io +// resources: +// - someresources +// - someresources/finalizers +// - someresources/status +// - someresources/scale +// verbs: +// - watch +// - list +// - create +func RenameResourceRule(rules *RewriteRules, obj []byte) ([]byte, error) { + var err error + + renameResources := false + obj, err = TransformArrayOfStrings(obj, "apiGroups", func(apiGroup string) string { + if rules.HasGroup(apiGroup) { + renameResources = true + return rules.RenameApiVersion(apiGroup) + } + if apiGroup == "*" { + renameResources = true + } + return apiGroup + }) + if err != nil { + return nil, err + } + + // Do not rename resources for unknown group. + if !renameResources { + return obj, nil + } + + return TransformArrayOfStrings(obj, "resources", func(resourceType string) string { + if resourceType == "*" || resourceType == "" { + return resourceType + } + + // Rename if there is rule for resourceType. + _, resRule := rules.GroupResourceRules(resourceType) + if resRule != nil { + return rules.RenameResource(resourceType) + } + return resourceType + }) +} + +// RestoreResourceRule restores apiGroups and resources in a single rule. +func RestoreResourceRule(rules *RewriteRules, obj []byte) ([]byte, error) { + var err error + + restoreResources := false + obj, err = TransformArrayOfStrings(obj, "apiGroups", func(apiGroup string) string { + if rules.IsRenamedGroup(apiGroup) { + restoreResources = true + return rules.RestoreApiVersion(apiGroup) + } + if apiGroup == "*" { + restoreResources = true + } + return apiGroup + }) + if err != nil { + return nil, err + } + + // Do not rename resources for unknown group. + if !restoreResources { + return obj, nil + } + + return TransformArrayOfStrings(obj, "resources", func(resourceType string) string { + if resourceType == "*" || resourceType == "" { + return resourceType + } + // Get rules for resource by restored resourceType. + originalResourceType := rules.RestoreResource(resourceType) + _, resRule := rules.GroupResourceRules(originalResourceType) + if resRule != nil { + // NOTE: subresource not trimmed. + return originalResourceType + } + + // No rules for resourceType, return as-is + return resourceType + }) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rbac_test.go b/images/kube-api-rewriter/pkg/rewriter/rbac_test.go new file mode 100644 index 0000000..9075b32 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rbac_test.go @@ -0,0 +1,184 @@ +/* +Copyright 2024 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 rewriter + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRenameRoleRule(t *testing.T) { + + tests := []struct { + name string + rule string + expect string + }{ + { + "group and resources", + `{"apiGroups":["original.group.io"], +"resources": ["someresources","someresources/finalizers","someresources/status"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["prefixed.resources.group.io"], +"resources": ["prefixedsomeresources","prefixedsomeresources/finalizers","prefixedsomeresources/status"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "only resources", + `{"apiGroups":["*"], +"resources": ["someresources","someresources/finalizers","someresources/status"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["*"], +"resources": ["prefixedsomeresources","prefixedsomeresources/finalizers","prefixedsomeresources/status"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "only group", + `{"apiGroups":["original.group.io"], +"resources": ["*"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["prefixed.resources.group.io"], +"resources": ["*"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "several groups", + `{"apiGroups":["original.group.io","other.group.io"], +"resources": ["*"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["prefixed.resources.group.io","other.prefixed.resources.group.io"], +"resources": ["*"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "allow all", + `{"apiGroups":["*"], "resources":["*"], "verbs":["*"]}`, + `{"apiGroups":["*"], "resources":["*"], "verbs":["*"]}`, + }, + { + "unknown group", + `{"apiGroups":["unknown.group.io"], "resources":["someresources"], "verbs":["*"]}`, + `{"apiGroups":["unknown.group.io"], "resources":["someresources"], "verbs":["*"]}`, + }, + { + "core resource", + `{"apiGroups":[""], "resources":["pods"], "verbs":["create"]}`, + `{"apiGroups":[""], "resources":["pods"], "verbs":["create"]}`, + }, + } + + rwr := createTestRewriter() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resBytes, err := RenameResourceRule(rwr.Rules, []byte(tt.rule)) + require.NoError(t, err, "should rename rule") + + actual := string(resBytes) + require.Equal(t, tt.expect, actual) + }) + } +} + +func TestRestoreRoleRule(t *testing.T) { + tests := []struct { + name string + rule string + expect string + }{ + { + "group and resources", + `{"apiGroups":["prefixed.resources.group.io"], +"resources": ["prefixedsomeresources","prefixedsomeresources/finalizers","prefixedsomeresources/status"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["original.group.io"], +"resources": ["someresources","someresources/finalizers","someresources/status"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "only resources", + `{"apiGroups":["*"], +"resources": ["prefixedsomeresources","prefixedsomeresources/finalizers","prefixedsomeresources/status"], +"verbs": ["watch", "list", "create"] +}`, + `{"apiGroups":["*"], +"resources": ["someresources","someresources/finalizers","someresources/status"], +"verbs": ["watch", "list", "create"] +}`, + }, + { + "only group", + `{"apiGroups":["prefixed.resources.group.io"], + "resources": ["*"], + "verbs": ["watch", "list", "create"] + }`, + `{"apiGroups":["original.group.io"], + "resources": ["*"], + "verbs": ["watch", "list", "create"] + }`, + }, + { + "several groups", + `{"apiGroups":["prefixed.resources.group.io","other.prefixed.resources.group.io"], + "resources": ["*"], + "verbs": ["watch", "list", "create"] + }`, + `{"apiGroups":["original.group.io","other.group.io"], + "resources": ["*"], + "verbs": ["watch", "list", "create"] + }`, + }, + { + "allow all", + `{"apiGroups":["*"], "resources":["*"], "verbs":["*"]}`, + `{"apiGroups":["*"], "resources":["*"], "verbs":["*"]}`, + }, + { + "unknown group", + `{"apiGroups":["unknown.group.io"], "resources":["someresources"], "verbs":["*"]}`, + `{"apiGroups":["unknown.group.io"], "resources":["someresources"], "verbs":["*"]}`, + }, + { + "core resource", + `{"apiGroups":[""], "resources":["pods","configmaps"], "verbs":["create"]}`, + `{"apiGroups":[""], "resources":["pods","configmaps"], "verbs":["create"]}`, + }, + } + + rwr := createTestRewriter() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resBytes, err := RestoreResourceRule(rwr.Rules, []byte(tt.rule)) + require.NoError(t, err, "should rename rule") + + actual := string(resBytes) + require.Equal(t, tt.expect, actual) + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/resource.go b/images/kube-api-rewriter/pkg/rewriter/resource.go new file mode 100644 index 0000000..e09c2de --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/resource.go @@ -0,0 +1,161 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +func RewriteCustomResourceOrList(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + kind := gjson.GetBytes(obj, "kind").String() + if action == Restore { + kind = rules.RestoreKind(kind) + } + origGroupName, origResName, isList := rules.ResourceByKind(kind) + if origGroupName == "" && origResName == "" { + // Return as-is if kind is not in rules. + return obj, nil + } + if isList { + if action == Restore { + return RestoreResourcesList(rules, obj) + } + + return RenameResourcesList(rules, obj) + } + + // Responses of GET, LIST, DELETE requests. + // AdmissionReview requests from API Server. + if action == Restore { + return RestoreResource(rules, obj) + } + // CREATE, UPDATE, PATCH requests. + // TODO need to implement for + return RenameResource(rules, obj) +} + +func RenameResourcesList(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RenameAPIVersionAndKind(rules, obj) + if err != nil { + return nil, err + } + + // Rewrite apiVersion and kind in each item. + return RewriteArray(obj, "items", func(singleResource []byte) ([]byte, error) { + return RenameResource(rules, singleResource) + }) +} + +func RestoreResourcesList(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RestoreAPIVersionAndKind(rules, obj) + if err != nil { + return nil, err + } + + // Restore apiVersion and kind in each item. + return RewriteArray(obj, "items", func(singleResource []byte) ([]byte, error) { + return RestoreResource(rules, singleResource) + }) +} + +func RenameResource(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RenameAPIVersionAndKind(rules, obj) + if err != nil { + return nil, err + } + + // Rewrite apiVersion in each managedFields. + return RenameManagedFields(rules, obj) +} + +func RestoreResource(rules *RewriteRules, obj []byte) ([]byte, error) { + obj, err := RestoreAPIVersionAndKind(rules, obj) + if err != nil { + return nil, err + } + + // Rewrite apiVersion in each managedFields. + return RestoreManagedFields(rules, obj) +} + +func RenameAPIVersionAndKind(rules *RewriteRules, obj []byte) ([]byte, error) { + apiVersion := gjson.GetBytes(obj, "apiVersion").String() + obj, err := sjson.SetBytes(obj, "apiVersion", rules.RenameApiVersion(apiVersion)) + if err != nil { + return nil, err + } + + kind := gjson.GetBytes(obj, "kind").String() + return sjson.SetBytes(obj, "kind", rules.RenameKind(kind)) +} + +func RestoreAPIVersionAndKind(rules *RewriteRules, obj []byte) ([]byte, error) { + apiVersion := gjson.GetBytes(obj, "apiVersion").String() + apiVersion = rules.RestoreApiVersion(apiVersion) + obj, err := sjson.SetBytes(obj, "apiVersion", apiVersion) + if err != nil { + return nil, err + } + + kind := gjson.GetBytes(obj, "kind").String() + return sjson.SetBytes(obj, "kind", rules.RestoreKind(kind)) +} + +func RewriteOwnerReferences(rules *RewriteRules, obj []byte, path string, action Action) ([]byte, error) { + return RewriteArray(obj, path, func(ownerRefObj []byte) ([]byte, error) { + return RewriteAPIVersionAndKind(rules, ownerRefObj, action) + }) +} + +// RestoreManagedFields restores apiVersion in managedFields items. +// +// Example response from the server: +// +// "metadata": { +// "managedFields":[ +// { "apiVersion":"renamed.resource.group.io/v1", "fieldsType":"FieldsV1", "fieldsV1":{ ... }}, "manager": "Go-http-client", ...}, +// { "apiVersion":"renamed.resource.group.io/v1", "fieldsType":"FieldsV1", "fieldsV1":{ ... }}, "manager": "kubectl-edit", ...} +// ], +func RestoreManagedFields(rules *RewriteRules, obj []byte) ([]byte, error) { + return RewriteArray(obj, "metadata.managedFields", func(managedField []byte) ([]byte, error) { + return TransformString(managedField, "apiVersion", func(apiVersion string) string { + return rules.RestoreApiVersion(apiVersion) + }) + }) +} + +// RenameManagedFields renames apiVersion in managedFields items. +// +// Example request from the client: +// +// "metadata": { +// "managedFields":[ +// { "apiVersion":"original.group.io/v1", "fieldsType":"FieldsV1", "fieldsV1":{ ... }}, "manager": "Go-http-client", ...}, +// { "apiVersion":"original.group.io/v1", "fieldsType":"FieldsV1", "fieldsV1":{ ... }}, "manager": "kubectl-edit", ...} +// ], +func RenameManagedFields(rules *RewriteRules, obj []byte) ([]byte, error) { + return RewriteArray(obj, "metadata.managedFields", func(managedField []byte) ([]byte, error) { + return TransformString(managedField, "apiVersion", func(apiVersion string) string { + return rules.RenameApiVersion(apiVersion) + }) + }) +} + +func RenameResourcePatch(rules *RewriteRules, patch []byte) ([]byte, error) { + return RenameMetadataPatch(rules, patch) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/resource_test.go b/images/kube-api-rewriter/pkg/rewriter/resource_test.go new file mode 100644 index 0000000..696717f --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/resource_test.go @@ -0,0 +1,383 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestRewriteMetadata(t *testing.T) { + tests := []struct { + name string + obj client.Object + newObj client.Object + action Action + expectLabels map[string]string + expectAnnotations map[string]string + }{ + { + "rename labels on Pod", + &corev1.Pod{ + TypeMeta: metav1.TypeMeta{ + Kind: "Pod", + APIVersion: "v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "bar", + Labels: map[string]string{ + "labelgroup.io": "labelvalue", + "component.labelgroup.io/labelkey": "labelvalue", + }, + Annotations: map[string]string{ + "annogroup.io": "annovalue", + }, + }, + }, + &corev1.Pod{}, + Rename, + map[string]string{ + "replacedlabelgroup.io": "labelvalue", + "component.replacedlabelgroup.io/labelkey": "labelvalue", + }, + map[string]string{ + "replacedanno.io": "annovalue", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.NotNil(t, tt.obj, "should not be nil") + + rwr := createTestRewriter() + bytes, err := json.Marshal(tt.obj) + require.NoError(t, err, "should marshal object %q %s/%s", tt.obj.GetObjectKind().GroupVersionKind().Kind, tt.obj.GetName(), tt.obj.GetNamespace()) + + rwBytes, err := TransformObject(bytes, "metadata", func(metadataObj []byte) ([]byte, error) { + return RewriteMetadata(rwr.Rules, metadataObj, tt.action) + }) + require.NoError(t, err, "should rewrite object") + + err = json.Unmarshal(rwBytes, &tt.newObj) + + require.NoError(t, err, "should unmarshal object") + + require.Equal(t, tt.expectLabels, tt.newObj.GetLabels(), "expect rewrite labels '%v' to be '%s', got '%s'", tt.obj.GetLabels(), tt.expectLabels, tt.newObj.GetLabels()) + require.Equal(t, tt.expectAnnotations, tt.newObj.GetAnnotations(), "expect rewrite annotations '%v' to be '%s', got '%s'", tt.obj.GetAnnotations(), tt.expectAnnotations, tt.newObj.GetAnnotations()) + }) + } +} + +func TestRestoreKnownCustomResourceList(t *testing.T) { + listKnownCR := `GET /apis/original.group.io/v1/someresources HTTP/1.1 +Host: 127.0.0.1 + +` + responseBody := `{ +"kind":"PrefixedSomeResourceList", +"apiVersion":"prefixed.resources.group.io/v1", +"metadata":{"resourceVersion":"412742959"}, +"items":[ + { + "metadata": { + "name": "resource-name", + "namespace": "ns-name", + "labels": { + "component.replacedlabelgroup.io/labelName": "labelValue" + }, + "annotations":{ + "replacedanno.io": "annoValue" + }, + "ownerReferences": [ + { + "apiVersion": "prefixed.resources.group.io/v1", + "kind": "PrefixedSomeResource", + "name": "owner-name", + "uid": "30b43f23-0c36-442f-897f-fececdf54620", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "other.product.group.io/v1alpha1", + "kind": "SomeResource", + "name": "another-owner-name", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "data": {"somekey":"somevalue"} + } +]}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(listKnownCR))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + require.Equal(t, "original.group.io", targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(responseBody), Restore) + if err != nil { + t.Fatalf("should restore RevisionControllerList without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore RevisionControllerList: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`kind`, "SomeResourceList"}, + {`items.0.metadata.labels.component\.replacedlabelgroup\.io/labelName`, ""}, + {`items.0.metadata.labels.component\.labelgroup\.io/labelName`, "labelValue"}, + {`items.0.metadata.annotations.replacedanno\.io`, ""}, + {`items.0.metadata.annotations.annogroup\.io`, "annoValue"}, + {`items.0.metadata.ownerReferences.0.apiVersion`, "original.group.io/v1"}, + {`items.0.metadata.ownerReferences.0.kind`, "SomeResource"}, + // "other.progduct.group.io" is not known for rules, this ownerRef should not be rewritten. + {`items.0.metadata.ownerReferences.1.apiVersion`, "other.product.group.io/v1alpha1"}, + {`items.0.metadata.ownerReferences.1.kind`, "SomeResource"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Log(string(resultBytes)) + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} + +// TODO this rewrite will be enabled later. Uncomment TestRestoreUnknownCustomResourceListWithKnownKind after enabling. +func TestNoRewriteForUnknownCustomResourceListWithKnownKind(t *testing.T) { + // Request list of resources with known kind but with unknown apiGroup. + // Check that RestoreResourceList will not rewrite apiVersion. + listUnknownCR := `GET /apis/other.product.group.io/v1alpha1/someresources HTTP/1.1 +Host: 127.0.0.1 + +` + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(listUnknownCR))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + + require.False(t, targetReq.ShouldRewriteRequest(), "should not rewrite request") + require.False(t, targetReq.ShouldRewriteResponse(), "should not rewrite response") +} + +// TODO Uncomment after enabling rewrite detection by apiVersion/kind for all resources. +/* +func TestRestoreUnknownCustomResourceListWithKnownKind(t *testing.T) { + // Request list of resources with known kind but with unknown apiGroup. + // Check that RestoreResourceList will not rewrite apiVersion. + listUnknownCR := `GET /apis/other.product.group.io/v1alpha1/someresources HTTP/1.1 +Host: 127.0.0.1 + +` + responseBody := `{ +"kind":"SomeResourceList", +"apiVersion":"other.product.group.io/v1alpha1", +"metadata":{"resourceVersion":"412742959"}, +"items":[ + { + "metadata": { + "name": "resource-name", + "namespace": "ns-name", + "labels": { + "component.replacedlabelgroup.io/labelName": "labelValue" + }, + "annotations":{ + "replacedanno.io": "annoValue" + }, + "ownerReferences": [ + { + "apiVersion": "prefixed.resources.group.io/v1", + "kind": "PrefixedSomeResource", + "name": "owner-name", + "uid": "30b43f23-0c36-442f-897f-fececdf54620", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "other.product.group.io/v1alpha1", + "kind": "SomeResource", + "name": "another-owner-name", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "data": {"somekey":"somevalue"} + } +]}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(listUnknownCR))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + + require.False(t, targetReq.ShouldRewriteRequest(), "should not rewrite request") + require.False(t, targetReq.ShouldRewriteResponse(), "should not rewrite response") + + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + require.Equal(t, "original.group.io", targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(responseBody), Restore) + if err != nil { + t.Fatalf("should restore RevisionControllerList without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore RevisionControllerList: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`kind`, "SomeResourceList"}, + {`apiVersion`, "other.product.group.io/v1alpha1"}, + {`items.0.metadata.labels.component\.replacedlabelgroup\.io/labelName`, ""}, + {`items.0.metadata.labels.component\.labelgroup\.io/labelName`, "labelValue"}, + {`items.0.metadata.annotations.replacedanno\.io`, ""}, + {`items.0.metadata.annotations.annogroup\.io`, "annoValue"}, + {`items.0.metadata.ownerReferences.0.apiVersion`, "original.group.io/v1"}, + {`items.0.metadata.ownerReferences.0.kind`, "SomeResource"}, + // "other.progduct.group.io" is not known for rules, this ownerRef should not be rewritten. + {`items.0.metadata.ownerReferences.1.apiVersion`, "other.product.group.io/v1alpha1"}, + {`items.0.metadata.ownerReferences.1.kind`, "SomeResource"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Log(string(resultBytes)) + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} +*/ + +func TestRenameKnownCustomResource(t *testing.T) { + postControllerRevision := `POST /apis/original.group.io/v1/someresources/namespaces/ns-name/resource-name HTTP/1.1 +Host: 127.0.0.1 + +` + requestBody := `{ +"kind":"SomeResource", +"apiVersion":"original.group.io/v1", +"metadata": { + "name": "resource-name", + "namespace": "ns-name", + "labels": { + "component.labelgroup.io/labelName": "labelValue" + }, + "annotations":{ + "annogroup.io": "annoValue" + }, + "ownerReferences": [ + { + "apiVersion": "original.group.io/v1", + "kind": "SomeResource", + "name": "owner-name", + "uid": "30b43f23-0c36-442f-897f-fececdf54620", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "other.product.group.io/v1alpha1", + "kind": "SomeResource", + "name": "another-owner-name", + "controller": true, + "blockOwnerDeletion": true + } + ] +}, +"data": {"somekey":"somevalue"} +}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(postControllerRevision + requestBody))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(requestBody), Rename) + if err != nil { + t.Fatalf("should rename SomeResource without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename SomeResource: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`kind`, "PrefixedSomeResource"}, + {`metadata.labels.component\.replacedlabelgroup\.io/labelName`, "labelValue"}, + {`metadata.labels.component\.labelgroup\.io/labelName`, ""}, + {`metadata.annotations.replacedanno\.io`, "annoValue"}, + {`metadata.annotations.annogroup\.io`, ""}, + {`metadata.ownerReferences.0.apiVersion`, "prefixed.resources.group.io/v1"}, + {`metadata.ownerReferences.0.kind`, "PrefixedSomeResource"}, + // "other.progduct.group.io" is not known for rules, this ownerRef should not be rewritten. + {`metadata.ownerReferences.1.apiVersion`, "other.product.group.io/v1alpha1"}, + {`metadata.ownerReferences.1.kind`, "SomeResource"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Log(string(resultBytes)) + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go new file mode 100644 index 0000000..ba17eea --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go @@ -0,0 +1,426 @@ +/* +Copyright 2024 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 rewriter + +import ( + "net/url" + "regexp" + "strings" + + "github.com/tidwall/gjson" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type RuleBasedRewriter struct { + Rules *RewriteRules +} + +type Action string + +const ( + // Restore is an action to restore resources to original. + Restore Action = "restore" + // Rename is an action to rename original resources. + Rename Action = "rename" +) + +// RewriteAPIEndpoint renames group and resource in /apis/* endpoints. +// It assumes that ep contains original group and resourceType. +// Restoring of path is not implemented. +func (rw *RuleBasedRewriter) RewriteAPIEndpoint(ep *APIEndpoint) *APIEndpoint { + var rwrEndpoint *APIEndpoint + + switch { + case ep.IsRoot || ep.IsCore || ep.IsUnknown: + // Leave paths /, /api, /api/*, and unknown paths as is. + case ep.IsCRD: + // Rename CRD name resourcetype.group for resources with rules. + rwrEndpoint = rw.rewriteCRDEndpoint(ep.Clone()) + default: + // Rewrite group and resourceType parts for resources with rules. + rwrEndpoint = rw.rewriteCRApiEndpoint(ep.Clone()) + } + + rewritten := rwrEndpoint != nil + + if rwrEndpoint == nil { + rwrEndpoint = ep.Clone() + } + + // Rewrite key and values if query has labelSelector. + if strings.Contains(ep.RawQuery, "labelSelector") { + newRawQuery := rw.rewriteLabelSelector(rwrEndpoint.RawQuery) + if newRawQuery != rwrEndpoint.RawQuery { + rewritten = true + rwrEndpoint.RawQuery = newRawQuery + } + } + + if rewritten { + return rwrEndpoint + } + + return nil +} + +func (rw *RuleBasedRewriter) rewriteCRDEndpoint(ep *APIEndpoint) *APIEndpoint { + // Rewrite fieldSelector if CRD list is requested. + if ep.CRDGroup == "" && ep.CRDResourceType == "" { + if strings.Contains(ep.RawQuery, "metadata.name") { + // Rewrite name in field selector if any. + newQuery := rw.rewriteFieldSelector(ep.RawQuery) + if newQuery != "" { + res := ep.Clone() + res.RawQuery = newQuery + return res + } + } + return nil + } + + // Check if resource has rules + _, resourceRule := rw.Rules.ResourceRules(ep.CRDGroup, ep.CRDResourceType) + if resourceRule == nil { + // No rewrite for CRD without rules. + return nil + } + // Rewrite group and resourceType in CRD name. + res := ep.Clone() + res.CRDGroup = rw.Rules.RenameApiVersion(ep.CRDGroup) + res.CRDResourceType = rw.Rules.RenameResource(res.CRDResourceType) + res.Name = res.CRDResourceType + "." + res.CRDGroup + return res +} + +func (rw *RuleBasedRewriter) rewriteCRApiEndpoint(ep *APIEndpoint) *APIEndpoint { + // Early return if request has no group, e.g. discovery. + if ep.Group == "" { + return nil + } + + // Rename group and resource for CR requests. + // Check if group has rules. Return early if not. + groupRule := rw.Rules.GroupRule(ep.Group) + if groupRule == nil { + // No group and resourceType rewrite for group without rules. + return nil + } + newGroup := rw.Rules.RenameApiVersion(ep.Group) + + // Shortcut: return clone if only group is requested. + newResource := "" + if ep.ResourceType != "" { + _, resRule := rw.Rules.ResourceRules(ep.Group, ep.ResourceType) + if resRule == nil { + // No group and resourceType rewrite for resourceType without rules. + return nil + } + newResource = rw.Rules.RenameResource(ep.ResourceType) + } + + // Return rewritten endpoint if group or resource are changed. + if newGroup != "" || newResource != "" { + res := ep.Clone() + if newGroup != "" { + res.Group = newGroup + } + if newResource != "" { + res.ResourceType = newResource + } + + return res + } + + return nil +} + +var metadataNameRe = regexp.MustCompile(`metadata.name\%3D([a-z0-9-]+)((\.[a-z0-9-]+)*)`) + +// rewriteFieldSelector rewrites value for metadata.name in fieldSelector of CRDs listing. +// Example request: +// https://APISERVER/apis/apiextensions.k8s.io/v1/customresourcedefinitions?fieldSelector=metadata.name%3Dresources.original.group.io&... +func (rw *RuleBasedRewriter) rewriteFieldSelector(rawQuery string) string { + matches := metadataNameRe.FindStringSubmatch(rawQuery) + if matches == nil { + return "" + } + + resourceType := matches[1] + group := matches[2] + group = strings.TrimPrefix(group, ".") + + _, resRule := rw.Rules.ResourceRules(group, resourceType) + if resRule == nil { + return "" + } + + group = rw.Rules.RenameApiVersion(group) + resourceType = rw.Rules.RenameResource(resourceType) + + newSelector := `metadata.name%3D` + resourceType + "." + group + + return metadataNameRe.ReplaceAllString(rawQuery, newSelector) +} + +// rewriteLabelSelector rewrites labels in labelSelector +// Example request: +// https:///apis/apps/v1/namespaces//deployments?labelSelector=app%3Dsomething +func (rw *RuleBasedRewriter) rewriteLabelSelector(rawQuery string) string { + q, err := url.ParseQuery(rawQuery) + if err != nil { + return rawQuery + } + lsq := q.Get("labelSelector") + if lsq == "" { + return rawQuery + } + + labelSelector, err := metav1.ParseToLabelSelector(lsq) + if err != nil { + // The labelSelector is not well-formed. We pass it through, so + // API Server will return an error. + return rawQuery + } + + // Return early if labelSelector is empty, e.g. ?labelSelector=&limit=500 + if labelSelector == nil { + return rawQuery + } + + rwrMatchLabels := rw.Rules.LabelsRewriter().RenameMap(labelSelector.MatchLabels) + + rwrMatchExpressions := make([]metav1.LabelSelectorRequirement, 0) + for _, expr := range labelSelector.MatchExpressions { + rwrExpr := expr + rwrExpr.Key, rwrExpr.Values = rw.Rules.LabelsRewriter().RewriteNameValues(rwrExpr.Key, rwrExpr.Values, Rename) + rwrMatchExpressions = append(rwrMatchExpressions, rwrExpr) + } + + rwrLabelSelector := &metav1.LabelSelector{ + MatchLabels: rwrMatchLabels, + MatchExpressions: rwrMatchExpressions, + } + + res, err := metav1.LabelSelectorAsSelector(rwrLabelSelector) + if err != nil { + return rawQuery + } + + q.Set("labelSelector", res.String()) + return q.Encode() +} + +// RewriteJSONPayload does rewrite based on kind. +// TODO(future refactor): Remove targetReq in all callers. +func (rw *RuleBasedRewriter) RewriteJSONPayload(_ *TargetRequest, obj []byte, action Action) ([]byte, error) { + // Detect Kind + kind := gjson.GetBytes(obj, "kind").String() + + var rwrBytes []byte + var err error + + obj, err = rw.FilterExcludes(obj, action) + if err != nil { + return obj, err + } + + switch kind { + case "APIGroupList": + rwrBytes, err = RewriteAPIGroupList(rw.Rules, obj) + + case "APIGroup": + rwrBytes, err = RewriteAPIGroup(rw.Rules, obj) + + case "APIResourceList": + rwrBytes, err = RewriteAPIResourceList(rw.Rules, obj) + + case "APIGroupDiscoveryList": + rwrBytes, err = RewriteAPIGroupDiscoveryList(rw.Rules, obj) + + case "AdmissionReview": + rwrBytes, err = RewriteAdmissionReview(rw.Rules, obj) + + case CRDKind, CRDListKind: + rwrBytes, err = RewriteCRDOrList(rw.Rules, obj, action) + + case MutatingWebhookConfigurationKind, + MutatingWebhookConfigurationListKind: + rwrBytes, err = RewriteMutatingOrList(rw.Rules, obj, action) + + case ValidatingWebhookConfigurationKind, + ValidatingWebhookConfigurationListKind: + rwrBytes, err = RewriteValidatingOrList(rw.Rules, obj, action) + + case EventKind, EventListKind: + rwrBytes, err = RewriteEventOrList(rw.Rules, obj, action) + + case ClusterRoleKind, ClusterRoleListKind: + rwrBytes, err = RewriteClusterRoleOrList(rw.Rules, obj, action) + + case RoleKind, RoleListKind: + rwrBytes, err = RewriteRoleOrList(rw.Rules, obj, action) + case DeploymentKind, DeploymentListKind: + rwrBytes, err = RewriteDeploymentOrList(rw.Rules, obj, action) + case StatefulSetKind, StatefulSetListKind: + rwrBytes, err = RewriteStatefulSetOrList(rw.Rules, obj, action) + case DaemonSetKind, DaemonSetListKind: + rwrBytes, err = RewriteDaemonSetOrList(rw.Rules, obj, action) + case PodKind, PodListKind: + rwrBytes, err = RewritePodOrList(rw.Rules, obj, action) + case PodDisruptionBudgetKind, PodDisruptionBudgetListKind: + rwrBytes, err = RewritePDBOrList(rw.Rules, obj, action) + case JobKind, JobListKind: + rwrBytes, err = RewriteJobOrList(rw.Rules, obj, action) + case ServiceKind, ServiceListKind: + rwrBytes, err = RewriteServiceOrList(rw.Rules, obj, action) + case PersistentVolumeClaimKind, PersistentVolumeClaimListKind: + rwrBytes, err = RewritePVCOrList(rw.Rules, obj, action) + + case ServiceMonitorKind, ServiceMonitorListKind: + rwrBytes, err = RewriteServiceMonitorOrList(rw.Rules, obj, action) + + case ValidatingAdmissionPolicyBindingKind, ValidatingAdmissionPolicyBindingListKind: + rwrBytes, err = RewriteValidatingAdmissionPolicyBindingOrList(rw.Rules, obj, action) + case ValidatingAdmissionPolicyKind, ValidatingAdmissionPolicyListKind: + rwrBytes, err = RewriteValidatingAdmissionPolicyOrList(rw.Rules, obj, action) + default: + // TODO Add rw.Rules.IsKnownKind() to rewrite only known kinds. + rwrBytes, err = RewriteCustomResourceOrList(rw.Rules, obj, action) + } + // Return obj bytes as-is in case of the error. + if err != nil { + return obj, err + } + + // Always rewrite metadata: labels, annotations, finalizers, ownerReferences. + // TODO: add rewriter for managedFields. + return RewriteResourceOrList2(rwrBytes, func(singleObj []byte) ([]byte, error) { + return TransformObject(singleObj, "metadata", func(metadataObj []byte) ([]byte, error) { + return RewriteMetadata(rw.Rules, metadataObj, action) + }) + }) +} + +// RestoreBookmark restores apiVersion and kind in an object in WatchEvent with type BOOKMARK. Bookmark is not a full object, so RewriteJSONPayload may add unexpected fields. +// Bookmark example: {"kind":"ConfigMap","apiVersion":"v1","metadata":{"resourceVersion":"438083871","creationTimestamp":null}} +func (rw *RuleBasedRewriter) RestoreBookmark(targetReq *TargetRequest, obj []byte) ([]byte, error) { + return RestoreAPIVersionAndKind(rw.Rules, obj) +} + +// RewritePatch rewrites patches for some known objects. +// Only rename action is required for patches. +func (rw *RuleBasedRewriter) RewritePatch(targetReq *TargetRequest, patchBytes []byte) ([]byte, error) { + _, resRule := rw.Rules.ResourceRules(targetReq.OrigGroup(), targetReq.OrigResourceType()) + if resRule != nil { + if targetReq.IsCRD() { + return RenameCRDPatch(rw.Rules, resRule, patchBytes) + } + return RenameResourcePatch(rw.Rules, patchBytes) + } + + switch targetReq.OrigResourceType() { + case "services": + return RenameServicePatch(rw.Rules, patchBytes) + case "deployments", + "daemonsets", + "statefulsets": + return RenameSpecTemplatePatch(rw.Rules, patchBytes) + case "validatingwebhookconfigurations", + "mutatingwebhookconfigurations": + return RenameWebhookConfigurationPatch(rw.Rules, patchBytes) + } + + return RenameMetadataPatch(rw.Rules, patchBytes) +} + +// FilterExcludes removes excluded resources from the list or return SkipItem if resource itself is excluded. +func (rw *RuleBasedRewriter) FilterExcludes(obj []byte, action Action) ([]byte, error) { + if action != Restore { + return obj, nil + } + + kind := gjson.GetBytes(obj, "kind").String() + if !isExcludableKind(kind) { + return obj, nil + } + + if rw.Rules.ShouldExclude(obj, kind) { + return obj, SkipItem + } + + // Also check each item if obj is List + if !strings.HasSuffix(kind, "List") { + return obj, nil + } + + singleKind := strings.TrimSuffix(kind, "List") + obj, err := RewriteResourceOrList2(obj, func(singleObj []byte) ([]byte, error) { + if rw.Rules.ShouldExclude(singleObj, singleKind) { + return nil, SkipItem + } + return nil, nil + }) + if err != nil { + return obj, err + } + return obj, nil +} + +func shouldRewriteOwnerReferences(resourceType string) bool { + switch resourceType { + case CRDKind, CRDListKind, + RoleKind, RoleListKind, + RoleBindingKind, RoleBindingListKind, + PodDisruptionBudgetKind, PodDisruptionBudgetListKind, + ControllerRevisionKind, ControllerRevisionListKind, + ClusterRoleKind, ClusterRoleListKind, + ClusterRoleBindingKind, ClusterRoleBindingListKind, + APIServiceKind, APIServiceListKind, + DeploymentKind, DeploymentListKind, + DaemonSetKind, DaemonSetListKind, + StatefulSetKind, StatefulSetListKind, + PodKind, PodListKind, + JobKind, JobListKind, + ValidatingWebhookConfigurationKind, + ValidatingWebhookConfigurationListKind, + MutatingWebhookConfigurationKind, + MutatingWebhookConfigurationListKind, + ServiceKind, ServiceListKind, + PersistentVolumeClaimKind, PersistentVolumeClaimListKind, + PrometheusRuleKind, PrometheusRuleListKind, + ServiceMonitorKind, ServiceMonitorListKind: + return true + } + + return false +} + +// isExcludeKind returns true if kind may be excluded from rewriting. +// Discovery kinds and AdmissionReview have special schemas, it is sane to +// exclude resources in particular rewriters. +func isExcludableKind(kind string) bool { + switch kind { + case "APIGroupList", + "APIGroup", + "APIResourceList", + "APIGroupDiscoveryList", + "AdmissionReview": + return false + } + + return true +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rule_rewriter_test.go b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter_test.go new file mode 100644 index 0000000..bb6502a --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter_test.go @@ -0,0 +1,418 @@ +/* +Copyright 2024 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 rewriter + +import ( + "bufio" + "bytes" + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func createTestRewriter() *RuleBasedRewriter { + apiGroupRules := map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + Categories: []string{"all"}, + ShortNames: []string{"sr", "srs"}, + }, + "anotherresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + Plural: "anotherresources", + Singular: "anotherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"ar"}, + }, + }, + }, + "other.group.io": { + GroupRule: GroupRule{ + Group: "other.group.io", + Versions: []string{"v2alpha3"}, + PreferredVersion: "v2alpha3", + Renamed: "other.prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1", "v1alpha1"}, + PreferredVersion: "v1", + ShortNames: []string{"or"}, + }, + }, + }, + } + + webhookRules := map[string]WebhookRule{ + "/validate-prefixed-resources-group-io-v1-prefixedsomeresource": { + Path: "/validate-original-group-io-v1-someresource", + Group: "original.group.io", + Resource: "someresources", + }, + } + + rules := &RewriteRules{ + KindPrefix: "Prefixed", + ResourceTypePrefix: "prefixed", + ShortNamePrefix: "p", + Categories: []string{"prefixed"}, + Rules: apiGroupRules, + Webhooks: webhookRules, + Labels: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + {Original: "component.labelgroup.io", Renamed: "component.replacedlabelgroup.io"}, + }, + Names: []MetadataReplaceRule{ + {Original: "labelgroup.io", Renamed: "replacedlabelgroup.io"}, + { + Original: "labelgroup.io", OriginalValue: "labelValueToRename", + Renamed: "replacedlabelgroup.io", RenamedValue: "renamedLabelValue", + }, + }, + }, + Annotations: MetadataReplace{ + Names: []MetadataReplaceRule{ + {Original: "annogroup.io", Renamed: "replacedanno.io"}, + }, + }, + } + rules.Init() + return &RuleBasedRewriter{ + Rules: rules, + } +} + +func TestRewriteAPIEndpoint(t *testing.T) { + tests := []struct { + name string + path string + expectPath string + expectQuery string + }{ + { + "rewritable group", + "/apis/original.group.io", + "/apis/prefixed.resources.group.io", + "", + }, + { + "rewritable group and version", + "/apis/original.group.io/v1", + "/apis/prefixed.resources.group.io/v1", + "", + }, + { + "rewritable resource list", + "/apis/original.group.io/v1/someresources", + "/apis/prefixed.resources.group.io/v1/prefixedsomeresources", + "", + }, + { + "rewritable resource by name", + "/apis/original.group.io/v1/someresources/srname", + "/apis/prefixed.resources.group.io/v1/prefixedsomeresources/srname", + "", + }, + { + "rewritable resource status", + "/apis/original.group.io/v1/someresources/srname/status", + "/apis/prefixed.resources.group.io/v1/prefixedsomeresources/srname/status", + "", + }, + { + "rewritable CRD", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/someresources.original.group.io", + "/apis/apiextensions.k8s.io/v1/customresourcedefinitions/prefixedsomeresources.prefixed.resources.group.io", + "", + }, + { + "labelSelector one label name", + "/api/v1/namespaces/nsname/pods?labelSelector=labelgroup.io&limit=0", + "/api/v1/namespaces/nsname/pods", + "labelSelector=replacedlabelgroup.io&limit=0", + }, + { + "labelSelector one prefixed label", + "/api/v1/pods?labelSelector=labelgroup.io%2Fsome-attr&limit=500", + "/api/v1/pods", + "labelSelector=replacedlabelgroup.io%2Fsome-attr&limit=500", + }, + { + "labelSelector label name and value", + "/api/v1/namespaces/d8-virtualization/pods?labelSelector=labelgroup.io%3Dlabelvalue&limit=500", + "/api/v1/namespaces/d8-virtualization/pods", + "labelSelector=replacedlabelgroup.io%3Dlabelvalue&limit=500", + }, + { + "labelSelector prefixed label and value", + "/api/v1/namespaces/d8-virtualization/pods?labelSelector=component.labelgroup.io%2Fsome-attr%3Dlabelvalue&limit=500", + "/api/v1/namespaces/d8-virtualization/pods", + "labelSelector=component.replacedlabelgroup.io%2Fsome-attr%3Dlabelvalue&limit=500", + }, + { + "labelSelector label name not in values", + "/api/v1/namespaces/d8-virtualization/pods?labelSelector=labelgroup.io+notin+%28value-one%2Cvalue-two%29&limit=500", + "/api/v1/namespaces/d8-virtualization/pods", + "labelSelector=replacedlabelgroup.io+notin+%28value-one%2Cvalue-two%29&limit=500", + }, + { + "labelSelector label name for deployments", + "/apis/apps/v1/deployments?labelSelector=labelgroup.io+notin+%28value-one%2ClabelValue%29&limit=500", + "/apis/apps/v1/deployments", + "labelSelector=replacedlabelgroup.io+notin+%28labelValue%2Cvalue-one%29&limit=500", + }, + { + "labelSelector label name and renamed value", + "/api/v1/namespaces/d8-virtualization/pods?labelSelector=labelgroup.io%3DlabelValueToRename&limit=500", + "/api/v1/namespaces/d8-virtualization/pods", + "labelSelector=replacedlabelgroup.io%3DrenamedLabelValue&limit=500", + }, + { + "labelSelector label name and renamed values", + "/api/v1/namespaces/d8-virtualization/pods?labelSelector=labelgroup.io+notin+%28value-one%2ClabelValueToRename%29&limit=500", + "/api/v1/namespaces/d8-virtualization/pods", + "labelSelector=replacedlabelgroup.io+notin+%28renamedLabelValue%2Cvalue-one%29&limit=500", + }, + { + "labelSelector label name and renamed values for deployments", + "/apis/apps/v1/deployments?labelSelector=labelgroup.io+notin+%28value-one%2ClabelValueToRename%29&limit=500", + "/apis/apps/v1/deployments", + "labelSelector=replacedlabelgroup.io+notin+%28renamedLabelValue%2Cvalue-one%29&limit=500", + }, + { + "labelSelector label name and renamed values for validating admission policy binding", + "/apis/admissionregistration.k8s.io/v1/validatingadmissionpolicybindings?labelSelector=labelgroup.io+notin+%28value-one%2ClabelValueToRename%29&limit=500", + "/apis/admissionregistration.k8s.io/v1/validatingadmissionpolicybindings", + "labelSelector=replacedlabelgroup.io+notin+%28renamedLabelValue%2Cvalue-one%29&limit=500", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse(tt.path) + require.NoError(t, err, "should parse path '%s'", tt.path) + + ep := ParseAPIEndpoint(u) + rwr := createTestRewriter() + + newEp := rwr.RewriteAPIEndpoint(ep) + + if tt.expectPath == "" { + require.Nil(t, newEp, "should not rewrite path '%s', got %+v", tt.path, newEp) + } + require.NotNil(t, newEp, "should rewrite path '%s', got nil endpoint. Original ep: %#v", tt.path, ep) + + require.Equal(t, tt.expectPath, newEp.Path(), "expect rewrite for path '%s' to be '%s', got '%s', newEp: %#v", tt.path, tt.expectPath, newEp.Path(), newEp) + require.Equal(t, tt.expectQuery, newEp.RawQuery, "expect rewrite query for path %q to be '%s', got '%s', newEp: %#v", tt.path, tt.expectQuery, newEp.RawQuery, newEp) + }) + } + +} + +func TestRestoreControllerRevisionList(t *testing.T) { + getControllerRevisions := `GET /apis/apps/v1/controllerrevisions HTTP/1.1 +Host: 127.0.0.1 + +` + responseBody := `{ +"kind":"ControllerRevisionList", +"apiVersion":"apps/v1", +"metadata":{"resourceVersion":"412742959"}, +"items":[ + { + "metadata": { + "name": "resource-name", + "namespace": "ns-name", + "labels": { + "component.replacedlabelgroup.io/labelName": "labelValue" + }, + "annotations":{ + "replacedanno.io": "annoValue" + }, + "ownerReferences": [ + { + "apiVersion": "prefixed.resources.group.io/v1", + "kind": "PrefixedSomeResource", + "name": "owner-name", + "uid": "30b43f23-0c36-442f-897f-fececdf54620", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "other.product.group.io/v1alpha1", + "kind": "SomeResource", + "name": "another-owner-name", + "controller": true, + "blockOwnerDeletion": true + } + ] + }, + "data": {"somekey":"somevalue"} + } +]}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(getControllerRevisions))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + // require.Equal(t, origGroup, targetReq.OrigGroup(), "should set proper orig group") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(responseBody), Restore) + if err != nil { + t.Fatalf("should restore RevisionControllerList without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should restore RevisionControllerList: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`kind`, "ControllerRevisionList"}, + {`items.0.metadata.labels.component\.replacedlabelgroup\.io/labelName`, ""}, + {`items.0.metadata.labels.component\.labelgroup\.io/labelName`, "labelValue"}, + {`items.0.metadata.annotations.replacedanno\.io`, ""}, + {`items.0.metadata.annotations.annogroup\.io`, "annoValue"}, + {`items.0.metadata.ownerReferences.0.apiVersion`, "original.group.io/v1"}, + {`items.0.metadata.ownerReferences.0.kind`, "SomeResource"}, + // "other.progduct.group.io" is not known for rules, this ownerRef should not be rewritten. + {`items.0.metadata.ownerReferences.1.apiVersion`, "other.product.group.io/v1alpha1"}, + {`items.0.metadata.ownerReferences.1.kind`, "SomeResource"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Log(string(resultBytes)) + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} + +func TestRenameControllerRevision(t *testing.T) { + postControllerRevision := `POST /apis/apps/v1/controllerrevisions/namespaces/ns/ctrl-rev-name HTTP/1.1 +Host: 127.0.0.1 + +` + requestBody := `{ +"kind":"ControllerRevision", +"apiVersion":"apps/v1", +"metadata": { + "name": "resource-name", + "namespace": "ns-name", + "labels": { + "component.labelgroup.io/labelName": "labelValue" + }, + "annotations":{ + "annogroup.io": "annoValue" + }, + "ownerReferences": [ + { + "apiVersion": "original.group.io/v1", + "kind": "SomeResource", + "name": "owner-name", + "uid": "30b43f23-0c36-442f-897f-fececdf54620", + "controller": true, + "blockOwnerDeletion": true + }, + { + "apiVersion": "other.product.group.io/v1alpha1", + "kind": "SomeResource", + "name": "another-owner-name", + "controller": true, + "blockOwnerDeletion": true + } + ] +}, +"data": {"somekey":"somevalue"} +}` + + req, err := http.ReadRequest(bufio.NewReader(bytes.NewBufferString(postControllerRevision + requestBody))) + require.NoError(t, err, "should parse hardcoded http request") + require.NotNil(t, req.URL, "should parse url in hardcoded http request") + + rwr := createTestRewriter() + targetReq := NewTargetRequest(rwr, req) + require.NotNil(t, targetReq, "should get TargetRequest") + require.True(t, targetReq.ShouldRewriteRequest(), "should rewrite request") + require.True(t, targetReq.ShouldRewriteResponse(), "should rewrite response") + + resultBytes, err := rwr.RewriteJSONPayload(targetReq, []byte(requestBody), Rename) + if err != nil { + t.Fatalf("should rename RevisionController without error: %v", err) + } + if resultBytes == nil { + t.Fatalf("should rename RevisionController: %v", err) + } + + tests := []struct { + path string + expected string + }{ + {`kind`, "ControllerRevision"}, + {`metadata.labels.component\.replacedlabelgroup\.io/labelName`, "labelValue"}, + {`metadata.labels.component\.labelgroup\.io/labelName`, ""}, + {`metadata.annotations.replacedanno\.io`, "annoValue"}, + {`metadata.annotations.annogroup\.io`, ""}, + {`metadata.ownerReferences.0.apiVersion`, "prefixed.resources.group.io/v1"}, + {`metadata.ownerReferences.0.kind`, "PrefixedSomeResource"}, + // "other.progduct.group.io" is not known for rules, this ownerRef should not be rewritten. + {`metadata.ownerReferences.1.apiVersion`, "other.product.group.io/v1alpha1"}, + {`metadata.ownerReferences.1.kind`, "SomeResource"}, + } + + for _, tt := range tests { + t.Run(tt.path, func(t *testing.T) { + actual := gjson.GetBytes(resultBytes, tt.path).String() + if actual != tt.expected { + t.Log(string(resultBytes)) + t.Fatalf("%s value should be %s, got %s", tt.path, tt.expected, actual) + } + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rules.go b/images/kube-api-rewriter/pkg/rewriter/rules.go new file mode 100644 index 0000000..8e97333 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rules.go @@ -0,0 +1,405 @@ +/* +Copyright 2024 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 rewriter + +import ( + "strings" + + "github.com/tidwall/gjson" + + "github.com/deckhouse/kube-api-rewriter/pkg/rewriter/indexer" +) + +type RewriteRules struct { + KindPrefix string `json:"kindPrefix"` + ResourceTypePrefix string `json:"resourceTypePrefix"` + ShortNamePrefix string `json:"shortNamePrefix"` + Categories []string `json:"categories"` + Rules map[string]APIGroupRule `json:"rules"` + Webhooks map[string]WebhookRule `json:"webhooks"` + Labels MetadataReplace `json:"labels"` + Annotations MetadataReplace `json:"annotations"` + Finalizers MetadataReplace `json:"finalizers"` + Excludes []ExcludeRule `json:"excludes"` + + // TODO move these indexed rewriters into the RuleBasedRewriter. + labelsRewriter *PrefixedNameRewriter + annotationsRewriter *PrefixedNameRewriter + finalizersRewriter *PrefixedNameRewriter + + apiGroupsIndex *indexer.MapIndexer +} + +// Init should be called before using rules in the RuleBasedRewriter. +func (rr *RewriteRules) Init() { + rr.labelsRewriter = NewPrefixedNameRewriter(rr.Labels) + rr.annotationsRewriter = NewPrefixedNameRewriter(rr.Annotations) + rr.finalizersRewriter = NewPrefixedNameRewriter(rr.Finalizers) + + // Add all original Kinds and KindList as implicit excludes. + originalKinds := make([]string, 0) + for _, apiGroupRule := range rr.Rules { + for _, resourceRule := range apiGroupRule.ResourceRules { + originalKinds = append(originalKinds, resourceRule.Kind, resourceRule.ListKind) + } + } + if len(originalKinds) > 0 { + rr.Excludes = append(rr.Excludes, ExcludeRule{Kinds: originalKinds}) + } + + // Index apiGroups originals and their renames. + rr.apiGroupsIndex = indexer.NewMapIndexer() + for _, apiGroupRule := range rr.Rules { + rr.apiGroupsIndex.AddPair(apiGroupRule.GroupRule.Group, apiGroupRule.GroupRule.Renamed) + } +} + +type APIGroupRule struct { + GroupRule GroupRule `json:"groupRule"` + ResourceRules map[string]ResourceRule `json:"resourceRules"` +} + +type GroupRule struct { + Group string `json:"group"` + Versions []string `json:"versions"` + PreferredVersion string `json:"preferredVersion"` + Renamed string `json:"renamed"` +} + +type ResourceRule struct { + Kind string `json:"kind"` + ListKind string `json:"listKind"` + Plural string `json:"plural"` + Singular string `json:"singular"` + ShortNames []string `json:"shortNames"` + Categories []string `json:"categories"` + Versions []string `json:"versions"` + PreferredVersion string `json:"preferredVersion"` +} + +type WebhookRule struct { + Path string `json:"path"` + Group string `json:"group"` + Resource string `json:"resource"` +} + +type MetadataReplace struct { + Prefixes []MetadataReplaceRule + Names []MetadataReplaceRule +} + +type MetadataReplaceRule struct { + Original string `json:"original"` + Renamed string `json:"renamed"` + OriginalValue string `json:"originalValue"` + RenamedValue string `json:"renamedValue"` +} + +type ExcludeRule struct { + Kinds []string `json:"kinds"` + MatchNames []string `json:"matchNames"` + MatchLabels map[string]string `json:"matchLabels"` +} + +// GetAPIGroupList returns an array of groups in format applicable to use in APIGroupList: +// +// { +// name +// versions: [ { groupVersion, version } ... ] +// preferredVersion: { groupVersion, version } +// } +func (rr *RewriteRules) GetAPIGroupList() []interface{} { + groups := make([]interface{}, 0) + + for _, rGroup := range rr.Rules { + group := map[string]interface{}{ + "name": rGroup.GroupRule.Group, + "preferredVersion": map[string]interface{}{ + "groupVersion": rGroup.GroupRule.Group + "/" + rGroup.GroupRule.PreferredVersion, + "version": rGroup.GroupRule.PreferredVersion, + }, + } + versions := make([]interface{}, 0) + for _, ver := range rGroup.GroupRule.Versions { + versions = append(versions, map[string]interface{}{ + "groupVersion": rGroup.GroupRule.Group + "/" + ver, + "version": ver, + }) + } + group["versions"] = versions + groups = append(groups, group) + } + + return groups +} + +func (rr *RewriteRules) ResourceByKind(kind string) (string, string, bool) { + for groupName, group := range rr.Rules { + for resName, res := range group.ResourceRules { + if res.Kind == kind { + return groupName, resName, false + } + if res.ListKind == kind { + return groupName, resName, true + } + } + } + return "", "", false +} + +func (rr *RewriteRules) WebhookRule(path string) *WebhookRule { + if webhookRule, ok := rr.Webhooks[path]; ok { + return &webhookRule + } + return nil +} + +func (rr *RewriteRules) IsRenamedGroup(apiGroup string) bool { + // Trim version and delimeter. + apiGroup, _, _ = strings.Cut(apiGroup, "/") + return rr.apiGroupsIndex.IsRenamed(apiGroup) +} + +func (rr *RewriteRules) HasGroup(group string) bool { + // Trim version and delimeter. + group, _, _ = strings.Cut(group, "/") + _, ok := rr.Rules[group] + return ok +} + +func (rr *RewriteRules) GroupRule(group string) *GroupRule { + if groupRule, ok := rr.Rules[group]; ok { + return &groupRule.GroupRule + } + return nil +} + +// KindRules returns rule for group and resource by apiGroup and kind. +// apiGroup may be a group or a group with version. +func (rr *RewriteRules) KindRules(apiGroup, kind string) (*GroupRule, *ResourceRule) { + group, _, _ := strings.Cut(apiGroup, "/") + groupRule, ok := rr.Rules[group] + if !ok { + return nil, nil + } + + for _, resRule := range groupRule.ResourceRules { + if resRule.Kind == kind { + return &groupRule.GroupRule, &resRule + } + if resRule.ListKind == kind { + return &groupRule.GroupRule, &resRule + } + } + return nil, nil +} + +func (rr *RewriteRules) ResourceRules(apiGroup, resource string) (*GroupRule, *ResourceRule) { + group, _, _ := strings.Cut(apiGroup, "/") + groupRule, ok := rr.Rules[group] + if !ok { + return nil, nil + } + resource, _, _ = strings.Cut(resource, "/") + resourceRule, ok := rr.Rules[group].ResourceRules[resource] + if !ok { + return nil, nil + } + return &groupRule.GroupRule, &resourceRule +} + +func (rr *RewriteRules) GroupResourceRules(resourceType string) (*GroupRule, *ResourceRule) { + // Trim subresource and delimiter. + resourceType, _, _ = strings.Cut(resourceType, "/") + + for _, group := range rr.Rules { + for _, res := range group.ResourceRules { + if res.Plural == resourceType { + return &group.GroupRule, &res + } + } + } + return nil, nil +} + +func (rr *RewriteRules) GroupResourceRulesByKind(kind string) (*GroupRule, *ResourceRule) { + for _, group := range rr.Rules { + for _, res := range group.ResourceRules { + if res.Kind == kind { + return &group.GroupRule, &res + } + } + } + return nil, nil +} + +func (rr *RewriteRules) RenameResource(resource string) string { + return rr.ResourceTypePrefix + resource +} + +func (rr *RewriteRules) RenameKind(kind string) string { + return rr.KindPrefix + kind +} + +// RestoreResource restores renamed resource to its original state, keeping suffix. +// E.g. "prefixedsomeresources/scale" will be restored to "someresources/scale". +func (rr *RewriteRules) RestoreResource(resource string) string { + return strings.TrimPrefix(resource, rr.ResourceTypePrefix) +} + +func (rr *RewriteRules) RestoreKind(kind string) string { + return strings.TrimPrefix(kind, rr.KindPrefix) +} + +// RestoreApiVersion returns apiVersion with restored apiGroup part. +// It keeps with version suffix as-is if present. +func (rr *RewriteRules) RestoreApiVersion(apiVersion string) string { + apiGroup, version, found := strings.Cut(apiVersion, "/") + + // No version suffix find, consider apiVersion is only a group name. + if !found { + return rr.apiGroupsIndex.Restore(apiVersion) + } + + // Restore apiGroup part, keep version suffix. + return rr.apiGroupsIndex.Restore(apiGroup) + "/" + version +} + +// RenameApiVersion returns apiVersion with renamed apiGroup part. +// It keeps with version suffix as-is if present. +func (rr *RewriteRules) RenameApiVersion(apiVersion string) string { + apiGroup, version, found := strings.Cut(apiVersion, "/") + + // No version suffix find, consider apiVersion is only a group name. + if !found { + return rr.apiGroupsIndex.Rename(apiVersion) + } + + // Rename apiGroup part, keep version suffix. + return rr.apiGroupsIndex.Rename(apiGroup) + "/" + version +} + +func (rr *RewriteRules) RenameCategories(categories []string) []string { + if len(categories) == 0 { + return []string{} + } + return rr.Categories +} + +func (rr *RewriteRules) RestoreCategories(resourceRule *ResourceRule) []string { + if resourceRule == nil { + return []string{} + } + return resourceRule.Categories +} + +func (rr *RewriteRules) RenameShortName(shortName string) string { + return rr.ShortNamePrefix + shortName +} + +func (rr *RewriteRules) RestoreShortName(shortName string) string { + return strings.TrimPrefix(shortName, rr.ShortNamePrefix) +} + +func (rr *RewriteRules) RenameShortNames(shortNames []string) []string { + newNames := make([]string, 0, len(shortNames)) + for _, shortName := range shortNames { + newNames = append(newNames, rr.ShortNamePrefix+shortName) + } + return newNames +} + +func (rr *RewriteRules) RestoreShortNames(shortNames []string) []string { + newNames := make([]string, 0, len(shortNames)) + for _, shortName := range shortNames { + newNames = append(newNames, strings.TrimPrefix(shortName, rr.ShortNamePrefix)) + } + return newNames +} + +func (rr *RewriteRules) LabelsRewriter() *PrefixedNameRewriter { + return rr.labelsRewriter +} + +func (rr *RewriteRules) AnnotationsRewriter() *PrefixedNameRewriter { + return rr.annotationsRewriter +} + +func (rr *RewriteRules) FinalizersRewriter() *PrefixedNameRewriter { + return rr.finalizersRewriter +} + +// ShouldExclude returns true if object should be excluded from response back to the client. +// Set kind when obj has no kind, e.g. a list item. +func (rr *RewriteRules) ShouldExclude(obj []byte, kind string) bool { + for _, exclude := range rr.Excludes { + if exclude.Match(obj, kind) { + return true + } + } + return false +} + +// Match returns true if object matches all conditions in the exclude rule. +func (r ExcludeRule) Match(obj []byte, kind string) bool { + objKind := kind + if objKind == "" { + objKind = gjson.GetBytes(obj, "kind").String() + } + kindMatch := len(r.Kinds) == 0 + for _, kind := range r.Kinds { + if objKind == kind { + kindMatch = true + break + } + } + + objLabels := mapStringStringFromBytes(obj, "metadata.labels") + matchLabels := len(r.MatchLabels) == 0 || mapContainsMap(objLabels, r.MatchLabels) + + matchName := len(r.MatchNames) == 0 + objName := gjson.GetBytes(obj, "metadata.name").String() + for _, name := range r.MatchNames { + if objName == name { + matchName = true + break + } + } + + // Return true if every condition match. + return kindMatch && matchLabels && matchName +} + +func mapStringStringFromBytes(obj []byte, path string) map[string]string { + result := make(map[string]string) + for field, value := range gjson.GetBytes(obj, path).Map() { + result[field] = value.String() + } + return result +} + +func mapContainsMap(obj, match map[string]string) bool { + if len(match) == 0 { + return true + } + for k, v := range match { + if obj[k] != v { + return false + } + } + return true +} diff --git a/images/kube-api-rewriter/pkg/rewriter/rules_test.go b/images/kube-api-rewriter/pkg/rewriter/rules_test.go new file mode 100644 index 0000000..4415960 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/rules_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2024 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 rewriter + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func newTestExcludeRules() *RewriteRules { + rules := RewriteRules{ + Rules: map[string]APIGroupRule{ + "originalgroup.io": { + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + }, + }, + }, + "anothergroup.io": { + ResourceRules: map[string]ResourceRule{ + "anotheresources": { + Kind: "AnotherResource", + ListKind: "AnotherResourceList", + }, + }, + }, + }, + Excludes: []ExcludeRule{ + { + Kinds: []string{"RoleBinding"}, + MatchLabels: map[string]string{ + "labelName": "labelValue", + }, + }, + { + Kinds: []string{"Role"}, + MatchNames: []string{"role1", "role2"}, + }, + }, + } + rules.Init() + return &rules +} + +func TestExcludeRuleKindsOnly(t *testing.T) { + rules := newTestExcludeRules() + + tests := []struct { + name string + obj string + expectExcluded bool + }{ + { + "original kind SomeResource in excludes", + `{"kind":"SomeResource"}`, + true, + }, + { + "kind UnknownResource not in excludes", + `{"kind":"UnknownResource"}`, + false, + }, + { + "RoleBinding with label in excludes", + `{"kind":"RoleBinding","metadata":{"labels":{"labelName":"labelValue"}}}`, + true, + }, + { + "RoleBinding with label not in excludes", + `{"kind":"RoleBinding","metadata":{"labels":{"labelName":"nonExcludedValue"}}}`, + false, + }, + { + "Role with name in excludes", + `{"kind":"Role","metadata":{"name":"role1"}}`, + true, + }, + { + "Role with name not in excludes", + `{"kind":"Role","metadata":{"name":"role-not-excluded"}}`, + false, + }, + { + "RoleBinding with name as role in excludes", + `{"kind":"RoleBinding","metadata":{"name":"role1"}}`, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := rules.ShouldExclude([]byte(tt.obj), "") + + if tt.expectExcluded { + require.True(t, actual, "'%s' should be excluded. Not excluded obj: %s", tt.name, tt.obj) + } else { + require.False(t, actual, "'%s' should not be excluded. Excluded obj: %s", tt.name, tt.obj) + + } + }) + } +} diff --git a/images/kube-api-rewriter/pkg/rewriter/target_request.go b/images/kube-api-rewriter/pkg/rewriter/target_request.go new file mode 100644 index 0000000..deb2d3a --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/target_request.go @@ -0,0 +1,306 @@ +/* +Copyright 2024 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 rewriter + +import ( + "fmt" + "net/http" +) + +type TargetRequest struct { + originEndpoint *APIEndpoint + targetEndpoint *APIEndpoint + + webhookRule *WebhookRule +} + +func NewTargetRequest(rwr *RuleBasedRewriter, req *http.Request) *TargetRequest { + if req == nil || req.URL == nil { + return nil + } + + // Is it a request to the webhook? + webhookRule := rwr.Rules.WebhookRule(req.URL.Path) + if webhookRule != nil { + return &TargetRequest{ + webhookRule: webhookRule, + } + } + + apiEndpoint := ParseAPIEndpoint(req.URL) + if apiEndpoint == nil { + return nil + } + + // rewrite path if needed + targetEndpoint := rwr.RewriteAPIEndpoint(apiEndpoint) + + return &TargetRequest{ + originEndpoint: apiEndpoint, + targetEndpoint: targetEndpoint, + } +} + +// Path return possibly rewritten path for target endpoint. +func (tr *TargetRequest) Path() string { + if tr.targetEndpoint != nil { + return tr.targetEndpoint.Path() + } + if tr.originEndpoint != nil { + return tr.originEndpoint.Path() + } + if tr.webhookRule != nil { + return tr.webhookRule.Path + } + + return "" +} + +func (tr *TargetRequest) IsCore() bool { + if tr.originEndpoint != nil { + return tr.originEndpoint.IsCore + } + return false +} + +func (tr *TargetRequest) IsCRD() bool { + if tr.originEndpoint != nil { + return tr.originEndpoint.IsCRD + } + return false +} + +func (tr *TargetRequest) IsWatch() bool { + if tr.originEndpoint != nil { + return tr.originEndpoint.IsWatch + } + return false +} + +func (tr *TargetRequest) IsWebhook() bool { + return tr.webhookRule != nil +} + +func (tr *TargetRequest) OrigGroup() string { + if tr.IsCRD() { + return tr.originEndpoint.CRDGroup + } + if tr.originEndpoint != nil { + return tr.originEndpoint.Group + } + if tr.webhookRule != nil { + return tr.webhookRule.Group + } + return "" +} + +func (tr *TargetRequest) OrigResourceType() string { + if tr.IsCRD() { + return tr.originEndpoint.CRDResourceType + } + if tr.originEndpoint != nil { + return tr.originEndpoint.ResourceType + } + if tr.webhookRule != nil { + return tr.webhookRule.Resource + } + return "" +} + +func (tr *TargetRequest) RawQuery() string { + if tr.targetEndpoint != nil { + return tr.targetEndpoint.RawQuery + } + if tr.originEndpoint != nil { + return tr.originEndpoint.RawQuery + } + return "" +} + +func (tr *TargetRequest) RequestURI() string { + path := tr.Path() + query := tr.RawQuery() + if query == "" { + return path + } + return fmt.Sprint(path, "?", query) +} + +// ShouldRewriteRequest returns true if incoming payload should +// be rewritten. +func (tr *TargetRequest) ShouldRewriteRequest() bool { + // Consider known webhook should be rewritten. Unknown paths will be passed as-is. + if tr.webhookRule != nil { + return true + } + + if tr.originEndpoint != nil { + if tr.originEndpoint.IsRoot || tr.originEndpoint.IsUnknown { + return false + } + + if tr.targetEndpoint == nil { + // Pass resources without rules as is, except some special types. + + // Rewrite request body when creating CRD. + if tr.originEndpoint.ResourceType == "customresourcedefinitions" && tr.originEndpoint.Name == "" { + return true + } + + return shouldRewriteResource(tr.originEndpoint.ResourceType) + } + } + + // Payload should be inspected to decide if rewrite is required. + return true +} + +// ShouldRewriteResponse return true if response rewrite is needed. +// Response may be passed as is if false. +func (tr *TargetRequest) ShouldRewriteResponse() bool { + // If there is webhook rule, response should be rewritten. + if tr.webhookRule != nil { + return true + } + + if tr.originEndpoint == nil { + return false + } + + if tr.originEndpoint.IsRoot || tr.originEndpoint.IsUnknown { + return false + } + + if tr.originEndpoint.IsCRD { + // Rewrite CRD List. + if tr.originEndpoint.Name == "" { + return true + } + // Rewrite CRD if group and resource was rewritten. + if tr.originEndpoint.Name != "" && tr.targetEndpoint != nil { + return true + } + return false + } + + // Rewrite if path was rewritten for known resource. + if tr.targetEndpoint != nil { + return true + } + + // Rewrite response from /apis discovery. + if tr.originEndpoint.Group == "" { + return true + } + + return shouldRewriteResource(tr.originEndpoint.ResourceType) +} + +func (tr *TargetRequest) ResourceForLog() string { + if tr.webhookRule != nil { + return tr.webhookRule.Resource + } + if tr.originEndpoint != nil { + ep := tr.originEndpoint + if ep.IsRoot { + return "ROOT" + } + if ep.IsUnknown { + return "UKNOWN" + } + if ep.IsCore { + // /api + if ep.Version == "" { + return "APIVersions/core" + } + // /api/v1 + if ep.ResourceType == "" { + return "APIResourceList/core" + } + // /api/v1/RESOURCE/NAME/SUBRESOURCE + // /api/v1/namespaces/NS/status + // /api/v1/namespaces/NS/RESOURCE/NAME/SUBRESOURCE + if ep.Subresource != "" { + return ep.ResourceType + "/" + ep.Subresource + } + // /api/v1/RESOURCETYPE + // /api/v1/RESOURCETYPE/NAME + // /api/v1/namespaces + // /api/v1/namespaces/NAMESPACE + // /api/v1/namespaces/NAMESPACE/RESOURCETYPE + // /api/v1/namespaces/NAMESPACE/RESOURCETYPE/NAME + return ep.ResourceType + } + // /apis + if ep.Group == "" { + return "APIGroupList" + } + // /apis/GROUP + if ep.Version == "" { + return "APIGroup/" + ep.Group + } + // /apis/GROUP/VERSION + if ep.ResourceType == "" { + return "APIResourceList/" + ep.Group + } + // /apis/GROUP/VERSION/RESOURCETYPE/NAME/SUBRESOURCE + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME/SUBRESOURCE + if ep.Subresource != "" { + return ep.ResourceType + "/" + ep.Subresource + } + // /apis/GROUP/VERSION/RESOURCETYPE + // /apis/GROUP/VERSION/RESOURCETYPE/NAME + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE + // /apis/GROUP/VERSION/namespaces/NAMESPACE/RESOURCETYPE/NAME + return ep.ResourceType + } + + return "UNKNOWN" +} + +func shouldRewriteResource(resourceType string) bool { + switch resourceType { + case "nodes", + "pods", + "configmaps", + "secrets", + "services", + "serviceaccounts", + "mutatingwebhookconfigurations", + "validatingwebhookconfigurations", + "clusterroles", + "roles", + "rolebindings", + "clusterrolebindings", + "deployments", + "statefulsets", + "daemonsets", + "jobs", + "persistentvolumeclaims", + "prometheusrules", + "servicemonitors", + "poddisruptionbudgets", + "controllerrevisions", + "apiservices", + "validatingadmissionpolicybindings", + "validatingadmissionpolicies", + "events": + return true + } + + return false +} diff --git a/images/kube-api-rewriter/pkg/rewriter/transformers.go b/images/kube-api-rewriter/pkg/rewriter/transformers.go new file mode 100644 index 0000000..ef68ec8 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/transformers.go @@ -0,0 +1,111 @@ +/* +Copyright 2024 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 rewriter + +import ( + "encoding/json" + + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// TransformString transforms string value addressed by path. +func TransformString(obj []byte, path string, transformFn func(field string) string) ([]byte, error) { + pathStr := gjson.GetBytes(obj, path) + if !pathStr.Exists() { + return obj, nil + } + rwrString := transformFn(pathStr.String()) + return sjson.SetBytes(obj, path, rwrString) +} + +// TransformObject transforms object value addressed by path. +func TransformObject(obj []byte, path string, transformFn func(item []byte) ([]byte, error)) ([]byte, error) { + pathObj := gjson.GetBytes(obj, path) + if !pathObj.IsObject() { + return obj, nil + } + rwrObj, err := transformFn([]byte(pathObj.Raw)) + if err != nil { + return nil, err + } + return sjson.SetRawBytes(obj, path, rwrObj) +} + +// TransformArrayOfStrings transforms array value addressed by path. +func TransformArrayOfStrings(obj []byte, arrayPath string, transformFn func(item string) string) ([]byte, error) { + // Transform each item in list. Put back original items if transformFn returns nil bytes. + items := gjson.GetBytes(obj, arrayPath).Array() + if len(items) == 0 { + return obj, nil + } + rwrItems := make([]string, len(items)) + for i, item := range items { + rwrItems[i] = transformFn(item.String()) + } + + return sjson.SetBytes(obj, arrayPath, rwrItems) +} + +// TransformPatch treats obj as a JSON patch or Merge patch and calls +// a corresponding transformFn. +func TransformPatch( + obj []byte, + transformMerge func(mergePatch []byte) ([]byte, error), + transformJSON func(jsonPatch []byte) ([]byte, error)) ([]byte, error) { + if len(obj) == 0 { + return obj, nil + } + // Merge patch for Kubernetes resource is always starts with the curly bracket. + if string(obj[0]) == "{" && transformMerge != nil { + return transformMerge(obj) + } + + // JSON patch should start with the square bracket. + if string(obj[0]) == "[" && transformJSON != nil { + return RewriteArray(obj, Root, transformJSON) + } + + // Return patch as-is in other cases. + return obj, nil +} + +// Helpers for traversing JSON objects with support for root path. +// gjson supports @this, but sjson don't, so unique alias is used. + +const Root = "@ROOT" + +func GetBytes(obj []byte, path string) gjson.Result { + if path == Root { + return gjson.ParseBytes(obj) + } + return gjson.GetBytes(obj, path) +} + +func SetBytes(obj []byte, path string, value interface{}) ([]byte, error) { + if path == Root { + return json.Marshal(value) + } + return sjson.SetBytes(obj, path, value) +} + +func SetRawBytes(obj []byte, path string, value []byte) ([]byte, error) { + if path == Root { + return value, nil + } + return sjson.SetRawBytes(obj, path, value) +} diff --git a/images/kube-api-rewriter/pkg/rewriter/webhook.go b/images/kube-api-rewriter/pkg/rewriter/webhook.go new file mode 100644 index 0000000..dfa3c62 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/webhook.go @@ -0,0 +1,17 @@ +/* +Copyright 2024 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 rewriter diff --git a/images/kube-api-rewriter/pkg/server/http_server.go b/images/kube-api-rewriter/pkg/server/http_server.go new file mode 100644 index 0000000..b309716 --- /dev/null +++ b/images/kube-api-rewriter/pkg/server/http_server.go @@ -0,0 +1,158 @@ +/* +Copyright 2024 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 server + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + log "log/slog" + "net" + "net/http" + "sync" + + logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/tls/certmanager" +) + +// HTTPServer starts HTTP server with root handler using listen address. +// Implements Runnable interface to be able to stop server. +type HTTPServer struct { + InstanceDesc string + ListenAddr string + RootHandler http.Handler + CertManager certmanager.CertificateManager + Err error + + initLock sync.Mutex + stopped bool + + listener net.Listener + instance *http.Server +} + +// init checks if listen is possible and creates new HTTP server instance. +// initLock is used to avoid data races with the Stop method. +func (s *HTTPServer) init() bool { + s.initLock.Lock() + defer s.initLock.Unlock() + if s.stopped { + // Stop was called earlier. + return false + } + + l, err := net.Listen("tcp", s.ListenAddr) + if err != nil { + s.Err = err + log.Error(fmt.Sprintf("%s: listen on %s err: %s", s.InstanceDesc, s.ListenAddr, err)) + return false + } + s.listener = l + log.Info(fmt.Sprintf("%s: listen for incoming requests on %s", s.InstanceDesc, s.ListenAddr)) + + mux := http.NewServeMux() + mux.Handle("/", s.RootHandler) + + s.instance = &http.Server{ + Handler: mux, + } + return true +} + +func (s *HTTPServer) Start() { + if !s.init() { + return + } + + // Start serving HTTP requests, block until server instance stops or returns an error. + var err error + if s.CertManager != nil { + go s.CertManager.Start() + s.setupTLS() + err = s.instance.ServeTLS(s.listener, "", "") + } else { + err = s.instance.Serve(s.listener) + } + // Ignore closed error: it's a consequence of stop. + if err != nil { + switch { + case errors.Is(err, http.ErrServerClosed): + case errors.Is(err, net.ErrClosed): + default: + s.Err = err + } + } + return +} + +func (s *HTTPServer) setupTLS() { + s.instance.TLSConfig = &tls.Config{ + GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) { + cert := s.CertManager.Current() + if cert == nil { + return nil, errors.New("no server certificate, server is not yet ready to receive traffic") + } + return cert, nil + }, + } +} + +// Stop shutdowns HTTP server instance and close a done channel. +// Stop and init may be run in parallel, so initLock is used to wait until +// variables are initialized. +func (s *HTTPServer) Stop() { + s.initLock.Lock() + defer s.initLock.Unlock() + + if s.stopped { + return + } + s.stopped = true + + if s.CertManager != nil { + s.CertManager.Stop() + } + // Shutdown instance if it was initialized. + if s.instance != nil { + log.Info(fmt.Sprintf("%s: stop", s.InstanceDesc)) + err := s.instance.Shutdown(context.Background()) + // Ignore ErrClosed. + if err != nil { + switch { + case errors.Is(err, http.ErrServerClosed): + case errors.Is(err, net.ErrClosed): + case s.Err != nil: + // log error to not reset runtime error. + log.Error(fmt.Sprintf("%s: stop instance", s.InstanceDesc), logutil.SlogErr(err)) + default: + s.Err = err + } + } + } +} + +// ConstructListenAddr return ip:port with defaults. +func ConstructListenAddr(addr, port, defaultAddr, defaultPort string) string { + if addr == "" { + addr = defaultAddr + } + if port == "" { + port = defaultPort + } + return addr + ":" + port +} diff --git a/images/kube-api-rewriter/pkg/server/runnable_group.go b/images/kube-api-rewriter/pkg/server/runnable_group.go new file mode 100644 index 0000000..952c5b7 --- /dev/null +++ b/images/kube-api-rewriter/pkg/server/runnable_group.go @@ -0,0 +1,90 @@ +/* +Copyright 2024 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 server + +import ( + "sync" +) + +type Runnable interface { + Start() + Stop() +} + +// RunnableGroup is a group of Runnables that should run until one of them stops. +type RunnableGroup struct { + runnables []Runnable +} + +func NewRunnableGroup() *RunnableGroup { + return &RunnableGroup{ + runnables: make([]Runnable, 0), + } +} + +// Add register Runnable in a group. +// Note: not designed for parallel registering. +func (rg *RunnableGroup) Add(r Runnable) { + rg.runnables = append(rg.runnables, r) +} + +// Start starts all Runnables and stops all of them when at least one Runnable stops. +func (rg *RunnableGroup) Start() { + // Start all runnables. + oneStoppedCh := rg.startAll() + + // Block until one runnable is stopped. + <-oneStoppedCh + + // Wait until all Runnables stop. + rg.stopAll() +} + +// startAll calls Start for each Runnable in separate go routines. +// It waits until all go routines starts. +// It returns a channel, so caller can receive event when one of the Runnables stops. +func (rg *RunnableGroup) startAll() chan struct{} { + oneStopped := make(chan struct{}) + var closeOnce sync.Once + + for i := range rg.runnables { + r := rg.runnables[i] + go func() { + r.Start() + closeOnce.Do(func() { + close(oneStopped) + }) + }() + } + + return oneStopped +} + +// stopAll calls Stop for each Runnable in a separate go routine. +// It waits until all go routines starts. +func (rg *RunnableGroup) stopAll() { + var wg sync.WaitGroup + wg.Add(len(rg.runnables)) + for i := range rg.runnables { + r := rg.runnables[i] + go func() { + r.Stop() + wg.Done() + }() + } + wg.Wait() +} diff --git a/images/kube-api-rewriter/pkg/target/kubernetes.go b/images/kube-api-rewriter/pkg/target/kubernetes.go new file mode 100644 index 0000000..75416d2 --- /dev/null +++ b/images/kube-api-rewriter/pkg/target/kubernetes.go @@ -0,0 +1,55 @@ +/* +Copyright 2024 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 target + +import ( + "fmt" + "net/http" + "net/url" + + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client/config" +) + +type Kubernetes struct { + Config *rest.Config + Client *http.Client + APIServerURL *url.URL +} + +func NewKubernetesTarget() (*Kubernetes, error) { + var err error + k := &Kubernetes{} + + k.Config, err = config.GetConfig() + if err != nil { + return nil, fmt.Errorf("load Kubernetes client config: %w", err) + } + + // Configure HTTP client to Kubernetes API server. + k.Client, err = rest.HTTPClientFor(k.Config) + if err != nil { + return nil, fmt.Errorf("setup Kubernetes API http client: %w", err) + } + + k.APIServerURL, err = url.Parse(k.Config.Host) + if err != nil { + return nil, fmt.Errorf("parse API server URL: %w", err) + } + + return k, nil +} diff --git a/images/kube-api-rewriter/pkg/target/webhook.go b/images/kube-api-rewriter/pkg/target/webhook.go new file mode 100644 index 0000000..7c60e6f --- /dev/null +++ b/images/kube-api-rewriter/pkg/target/webhook.go @@ -0,0 +1,106 @@ +/* +Copyright 2024 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 target + +import ( + "crypto/tls" + "fmt" + "net" + "net/http" + "net/url" + "os" + "time" + + "github.com/deckhouse/kube-api-rewriter/pkg/tls/certmanager" + "github.com/deckhouse/kube-api-rewriter/pkg/tls/certmanager/filesystem" +) + +type Webhook struct { + Client *http.Client + URL *url.URL + CertManager certmanager.CertificateManager +} + +const ( + WebhookAddressVar = "WEBHOOK_ADDRESS" + WebhookServerNameVar = "WEBHOOK_SERVER_NAME" + WebhookCertFileVar = "WEBHOOK_CERT_FILE" + WebhookKeyFileVar = "WEBHOOK_KEY_FILE" +) + +var ( + defaultWebhookTimeout = 30 * time.Second + defaultWebhookAddress = "https://127.0.0.1:9443" +) + +func NewWebhookTarget() (*Webhook, error) { + var err error + webhook := &Webhook{} + + // Target address and serverName. + address := os.Getenv(WebhookAddressVar) + if address == "" { + address = defaultWebhookAddress + } + + serverName := os.Getenv(WebhookServerNameVar) + if serverName == "" { + serverName = address + } + + webhook.URL, err = url.Parse(address) + if err != nil { + return nil, err + } + + // Certificate settings. + certFile := os.Getenv(WebhookCertFileVar) + keyFile := os.Getenv(WebhookKeyFileVar) + if certFile == "" && keyFile != "" { + return nil, fmt.Errorf("should specify cert file in %s if %s is not empty", WebhookCertFileVar, WebhookKeyFileVar) + } + if certFile != "" && keyFile == "" { + return nil, fmt.Errorf("should specify key file in %s if %s is not empty", WebhookKeyFileVar, WebhookCertFileVar) + } + if certFile != "" && keyFile != "" { + webhook.CertManager = filesystem.NewFileCertificateManager(certFile, keyFile) + } + + // Construct TLS client without validation to connect to the local webhook server. + dialer := &net.Dialer{ + Timeout: defaultWebhookTimeout, + } + + tr := &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + ServerName: serverName, + }, + DisableKeepAlives: true, + IdleConnTimeout: 5 * time.Minute, + TLSHandshakeTimeout: 10 * time.Second, + ExpectContinueTimeout: 1 * time.Second, + DialContext: dialer.DialContext, + } + + webhook.Client = &http.Client{ + Transport: tr, + Timeout: defaultWebhookTimeout, + } + + return webhook, nil +} diff --git a/images/kube-api-rewriter/pkg/tls/certmanager/certmanager.go b/images/kube-api-rewriter/pkg/tls/certmanager/certmanager.go new file mode 100644 index 0000000..e10a8c4 --- /dev/null +++ b/images/kube-api-rewriter/pkg/tls/certmanager/certmanager.go @@ -0,0 +1,27 @@ +/* +Copyright 2024 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 certmanager + +import ( + "crypto/tls" +) + +type CertificateManager interface { + Start() + Stop() + Current() *tls.Certificate +} diff --git a/images/kube-api-rewriter/pkg/tls/certmanager/filesystem/file-cert-manager.go b/images/kube-api-rewriter/pkg/tls/certmanager/filesystem/file-cert-manager.go new file mode 100644 index 0000000..1f6d7fc --- /dev/null +++ b/images/kube-api-rewriter/pkg/tls/certmanager/filesystem/file-cert-manager.go @@ -0,0 +1,170 @@ +/* +Copyright 2024 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 filesystem + +import ( + "crypto/tls" + "fmt" + "log/slog" + "os" + "path/filepath" + "sync" + "time" + + "github.com/fsnotify/fsnotify" + + logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" + "github.com/deckhouse/kube-api-rewriter/pkg/tls/util" +) + +type FileCertificateManager struct { + stopCh chan struct{} + certAccessLock sync.Mutex + cert *tls.Certificate + certBytesPath string + keyBytesPath string + errorRetryInterval time.Duration +} + +func NewFileCertificateManager(certBytesPath, keyBytesPath string) *FileCertificateManager { + return &FileCertificateManager{ + certBytesPath: certBytesPath, + keyBytesPath: keyBytesPath, + stopCh: make(chan struct{}), + errorRetryInterval: 1 * time.Minute, + } +} + +func (f *FileCertificateManager) Start() { + objectUpdated := make(chan struct{}, 1) + watcher, err := fsnotify.NewWatcher() + if err != nil { + slog.Error("failed to create an inotify watcher", logutil.SlogErr(err)) + } + defer watcher.Close() + + certDir := filepath.Dir(f.certBytesPath) + err = watcher.Add(certDir) + if err != nil { + slog.Error(fmt.Sprintf("failed to establish a watch on %s", f.certBytesPath), logutil.SlogErr(err)) + } + keyDir := filepath.Dir(f.keyBytesPath) + if keyDir != certDir { + err = watcher.Add(keyDir) + if err != nil { + slog.Error(fmt.Sprintf("failed to establish a watch on %s", f.keyBytesPath), logutil.SlogErr(err)) + } + } + + go func() { + for { + select { + case _, ok := <-watcher.Events: + if !ok { + return + } + select { + case objectUpdated <- struct{}{}: + default: + slog.Debug("Dropping redundant wakeup for cert reload") + } + case err, ok := <-watcher.Errors: + if !ok { + return + } + slog.Error(fmt.Sprintf("An error occurred when watching certificates files %s and %s", f.certBytesPath, f.keyBytesPath), logutil.SlogErr(err)) + } + } + }() + + // ensure we load the certificates on startup + objectUpdated <- struct{}{} + +sync: + for { + select { + case <-objectUpdated: + if err := f.rotateCerts(); err != nil { + go func() { + time.Sleep(f.errorRetryInterval) + select { + case objectUpdated <- struct{}{}: + default: + slog.Debug("Dropping redundant wakeup for cert reload") + } + }() + } + case <-f.stopCh: + break sync + } + } +} + +func (f *FileCertificateManager) Stop() { + f.certAccessLock.Lock() + defer f.certAccessLock.Unlock() + select { + case <-f.stopCh: + default: + close(f.stopCh) + } +} + +func (f *FileCertificateManager) rotateCerts() error { + crt, err := f.loadCertificates() + if err != nil { + return fmt.Errorf("failed to load the certificate %s and %s: %w", f.certBytesPath, f.keyBytesPath, err) + } + + f.certAccessLock.Lock() + defer f.certAccessLock.Unlock() + // update after the callback, to ensure that the reconfiguration succeeded + f.cert = crt + slog.Info(fmt.Sprintf("certificate with common name '%s' retrieved.", crt.Leaf.Subject.CommonName)) + return nil +} + +func (f *FileCertificateManager) loadCertificates() (serverCrt *tls.Certificate, err error) { + // #nosec No risk for path injection. Used for specific cert file for key rotation + certBytes, err := os.ReadFile(f.certBytesPath) + if err != nil { + return nil, err + } + // #nosec No risk for path injection. Used for specific cert file for key rotation + keyBytes, err := os.ReadFile(f.keyBytesPath) + if err != nil { + return nil, err + } + + crt, err := tls.X509KeyPair(certBytes, keyBytes) + if err != nil { + return nil, fmt.Errorf("failed to load certificate: %w\n", err) + } + + leaf, err := util.ParseCertsPEM(certBytes) + if err != nil { + return nil, fmt.Errorf("failed to load leaf certificate: %w\n", err) + } + crt.Leaf = leaf[0] + return &crt, nil +} + +func (f *FileCertificateManager) Current() *tls.Certificate { + f.certAccessLock.Lock() + defer f.certAccessLock.Unlock() + return f.cert +} diff --git a/images/kube-api-rewriter/pkg/tls/util/util.go b/images/kube-api-rewriter/pkg/tls/util/util.go new file mode 100644 index 0000000..7871dba --- /dev/null +++ b/images/kube-api-rewriter/pkg/tls/util/util.go @@ -0,0 +1,52 @@ +/* +Copyright 2024 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 util + +import ( + "crypto/x509" + "encoding/pem" + "errors" +) + +const CertificateBlockType string = "CERTIFICATE" + +func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + for len(pemCerts) > 0 { + var block *pem.Block + block, pemCerts = pem.Decode(pemCerts) + if block == nil { + break + } + // Only use PEM "CERTIFICATE" blocks without extra headers + if block.Type != CertificateBlockType || len(block.Headers) != 0 { + continue + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return certs, err + } + + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.New("data does not contain any valid RSA or ECDSA certificates") + } + return certs, nil +} diff --git a/images/kube-api-rewriter/werf.inc.yaml b/images/kube-api-rewriter/werf.inc.yaml new file mode 100644 index 0000000..3ff9afb --- /dev/null +++ b/images/kube-api-rewriter/werf.inc.yaml @@ -0,0 +1,64 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: + - add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/kube-api-rewriter + stageDependencies: + install: + - go.mod + - go.sum + - "**/*.go" +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-builder +final: false +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.24" "builder/golang-alt-svace-1.24" }} +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /src + before: install +secrets: +- id: GOPROXY + value: {{ .GOPROXY }} +mount: + - fromPath: ~/go-pkg-cache + to: /go/pkg +shell: + install: + - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /src/kube-api-rewriter + - go mod download + setup: + - cd /src/kube-api-rewriter + - export GOOS=linux + - export CGO_ENABLED=0 + - export GOARCH=amd64 + - | + {{- $_ := set $ "ProjectName" (list $.ImageName "kube-api-rewriter" | join "/") }} + {{- include "image-build.build" (set $ "BuildCommand" `go build -v -a -o kube-api-rewriter ./cmd/kube-api-rewriter`) | nindent 6 }} + +--- + +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromImage: builder/scratch +git: + {{- include "image mount points" . }} +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-builder + add: /src/kube-api-rewriter/kube-api-rewriter + to: /app/kube-api-rewriter + after: install + # Make containerd compatible directories structure. + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-builder + add: /var + to: /var + includePaths: + - run + after: install +imageSpec: + config: + user: "64535:64535" + workingDir: "/app" + entrypoint: ["/app/kube-api-rewriter"] diff --git a/images/nelm-source-controller/werf.inc.yaml b/images/nelm-source-controller/werf.inc.yaml new file mode 100644 index 0000000..dcec854 --- /dev/null +++ b/images/nelm-source-controller/werf.inc.yaml @@ -0,0 +1,3 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +from: registry.werf.io/nelm/source-controller:v0.1.4 diff --git a/module.yaml b/module.yaml new file mode 100644 index 0000000..c71920a --- /dev/null +++ b/module.yaml @@ -0,0 +1,25 @@ +name: operator-helm +stage: Experimental +# TODO: should we mark module as critical? +# weight: 960 +requirements: + deckhouse: ">= 1.69" +subsystems: + # TODO: confirm with TL + - delivery +namespace: d8-operator-helm +descriptions: + en: An operator to deploy helm applications declaratively. + ru: Оператор для декларативного развертывания helm-приложений. +# TODO: confirm with TL +tags: ["delivery"] +disable: + confirmation: true + message: "Disabling of this module can cause disruptions in deployed applications operation." +accessibility: + editions: + # TODO: confirm with TL + _default: + available: true + enabledInBundles: + - Minimal diff --git a/openapi/config-values.yaml b/openapi/config-values.yaml new file mode 100644 index 0000000..7097068 --- /dev/null +++ b/openapi/config-values.yaml @@ -0,0 +1,26 @@ +type: object +properties: + highAvailability: + type: boolean + x-examples: [true, false] + description: | + Manually enable the high availability (HA) mode. + + By default, Deckhouse automatically decides whether to enable the HA mode. + To learn more about the HA mode, refer to [High reliability and availability](/products/kubernetes-platform/documentation/v1/admin/configuration/high-reliability-and-availability/enable.html#enabling-ha-mode-for-individual-components). + logLevel: + type: string + default: info + description: | + Sets a logging level. + + Working for this components: + - `helm-controller` + - `nelm-source-controller` + - `kube-api-rewriter` + - `deckhouse-helm-controller` + enum: + - "debug" + - "info" + - "warn" + - "error" diff --git a/openapi/doc-ru-config-values.yaml b/openapi/doc-ru-config-values.yaml new file mode 100644 index 0000000..f8c368c --- /dev/null +++ b/openapi/doc-ru-config-values.yaml @@ -0,0 +1,24 @@ +type: object +properties: + highAvailability: + description: | + Ручное управление режимом отказоустойчивости. + + По умолчанию режим отказоустойчивости определяется автоматически. + Подробнее про режим отказоустойчивости можно прочитать в разделе [Высокая надежность и доступность](/products/kubernetes-platform/documentation/v1/admin/configuration/high-reliability-and-availability/enable.html#включение-режима-ha-для-отдельных-компонентов). + logLevel: + type: string + default: info + description: | + Устанавливает уровень логирования. + + Работает для следующих компонентов: + - `helm-controller` + - `nelm-source-controller` + - `kube-api-rewriter` + - `deckhouse-helm-controller` + enum: + - "debug" + - "info" + - "warn" + - "error" diff --git a/openapi/values.yaml b/openapi/values.yaml new file mode 100644 index 0000000..438606f --- /dev/null +++ b/openapi/values.yaml @@ -0,0 +1,20 @@ +x-extend: + schema: config-values.yaml +type: object +properties: + internal: + type: object + default: {} + properties: + moduleConfig: + type: object + additionalProperties: true + moduleConfigValidation: + type: object + properties: + error: + type: string + moduleState: + type: object + default: {} + additionalProperties: true diff --git a/oss.yaml b/oss.yaml new file mode 100644 index 0000000..3d56609 --- /dev/null +++ b/oss.yaml @@ -0,0 +1,12 @@ +- name: 3p-helm-controller + link: https://github.com/werf/3p-helm-controller + description: The helm-controller is a Kubernetes operator, allowing one to declaratively manage Helm chart releases. + license: Apache License 2.0 + version: v0.1.3 + id: 3p-helm-controller +- name: nelm-source-controller + link: https://github.com/werf/nelm-source-controller + description: The source-controller is a Kubernetes operator, specialised in artifacts acquisition from external sources such as Git, OCI, Helm repositories and S3-compatible buckets. + license: Apache License 2.0 + version: v0.1.4 + id: nelm-source-controller diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..8a9dc39 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,6 @@ +dependencies: +- name: deckhouse_lib_helm + repository: https://deckhouse.github.io/lib-helm + version: 1.55.1 +digest: sha256:5bdef3964d2672b8ff290f32e22569bc502e040e4e70274cab1762f27d9982e0 +generated: "2026-02-16T03:37:06.63855+03:00" diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl new file mode 100644 index 0000000..15d4c6b --- /dev/null +++ b/templates/_helpers.tpl @@ -0,0 +1,25 @@ +{{- /* Return logLevel as a string. */}} +{{- define "moduleLogLevel" -}} +{{- dig "logLevel" "" .Values.operatorHelm -}} +{{- end }} + +{{- define "hasValidModuleConfig" -}} +{{- if (hasKey .Values.operatorHelm.internal "moduleConfig" ) -}} +true +{{- end }} +{{- end }} + +{{- define "priorityClassName" -}} +system-cluster-critical +{{- end }} + +{{- define "vpa.policyUpdateMode" -}} +{{- $kubeVersion := .Values.global.discovery.kubernetesVersion -}} +{{- $updateMode := "" -}} +{{- if semverCompare ">=1.33.0" $kubeVersion -}} +{{- $updateMode = "InPlaceOrRecreate" -}} +{{- else -}} +{{- $updateMode = "Recreate" -}} +{{- end }} +{{- $updateMode }} +{{- end }} diff --git a/templates/helm-controller/_helpers.tpl b/templates/helm-controller/_helpers.tpl new file mode 100644 index 0000000..192ca22 --- /dev/null +++ b/templates/helm-controller/_helpers.tpl @@ -0,0 +1,6 @@ +{{- define "helm-controller.envs" -}} +- name: RUNTIME_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace +{{- end }} diff --git a/templates/helm-controller/deployment.yaml b/templates/helm-controller/deployment.yaml new file mode 100644 index 0000000..e333823 --- /dev/null +++ b/templates/helm-controller/deployment.yaml @@ -0,0 +1,137 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "helm_controller_resources" }} +cpu: 100m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: helm-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: helm-controller + minAllowed: + {{- include "helm_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: helm-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +spec: + replicas: {{ include "helm_lib_is_ha_to_value" (list . 3 1) }} + {{- if (include "helm_lib_ha_enabled" .) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + {{- end }} + revisionHistoryLimit: 2 + selector: + matchLabels: + app: helm-controller + template: + metadata: + labels: + app: helm-controller + annotations: + kubectl.kubernetes.io/default-container: helm-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: helm-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "helmController") }} + imagePullPolicy: IfNotPresent + args: + - --watch-all-namespaces + - --log-level={{ include "moduleLogLevel" . }} + - --log-encoding=json + - --enable-leader-election + volumeMounts: + - mountPath: /tmp + name: temp + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "helm_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + {{- include "helm-controller.envs" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /readyz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + {{- $kubeRbacProxySettings := dict }} + {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" false }} + {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} + {{- $_ := set $kubeRbacProxySettings "upstreams" (list + (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "helm-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: helm-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "helm-controller")) | nindent 6 }} + volumes: + - emptyDir: {} + name: temp + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/helm-controller/rbac-for-us.yaml b/templates/helm-controller/rbac-for-us.yaml new file mode 100644 index 0000000..34432c1 --- /dev/null +++ b/templates/helm-controller/rbac-for-us.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:helm-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:helm-controller +subjects: +- kind: ServiceAccount + name: helm-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:helm-controller +rules: +- apiGroups: ['*'] + resources: ['*'] + verbs: ['*'] +- nonResourceURLs: ['*'] + verbs: ['*'] +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmcharts + - internalnelmoperatorocirepositories + verbs: + - get + - list + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorocirepositories/status + verbs: + - get +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: helm-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: helm-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: helm-controller +subjects: +- kind: ServiceAccount + name: helm-controller diff --git a/templates/helm-controller/service-metrics.yaml b/templates/helm-controller/service-metrics.yaml new file mode 100644 index 0000000..1f2652c --- /dev/null +++ b/templates/helm-controller/service-metrics.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: helm-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: helm-controller diff --git a/templates/helm-controller/service-monitor.yaml b/templates/helm-controller/service-monitor.yaml new file mode 100644 index 0000000..8161d87 --- /dev/null +++ b/templates/helm-controller/service-monitor.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: helm-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "helm-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "helm-controller" diff --git a/templates/kube-api-rewriter/_customize_patch_helpers.tpl b/templates/kube-api-rewriter/_customize_patch_helpers.tpl new file mode 100644 index 0000000..72b1d18 --- /dev/null +++ b/templates/kube-api-rewriter/_customize_patch_helpers.tpl @@ -0,0 +1,69 @@ +{{- /* Helpers to create patches for component customizer in Kubevirt and CDI configurations. + +- kube_api_rewriter.pod_spec_strategic_patch_json - creates a JSON patch for a pod spec to add kube-api-rewriter sidecar container. +- kube_api_rewriter.service_spec_port_patch_json - creates a JSON patch for a service spec to point it to the kube-api-rewriter webhook proxy. +- kube_api_rewriter.webhook_spec_port_patch_json - creates a JSON patch for a validating or mutating webhook spec to point it to the kube-api-rewriter webhook proxy. + +*/ -}} + +{{- define "kube_api_rewriter.pod_spec_strategic_patch_json" -}} + '{{ include "kube_api_rewriter.pod_spec_strategic_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.pod_spec_strategic_patch" -}} + {{- $ctx := index . 0 -}} + {{- $mainContainerName := index . 1 -}} + {{- $settings := dict -}} + {{- if ge (len .) 3 -}} + {{- $settings = index . 2 -}} + {{- end -}} + {{- $isWebhook := hasKey $settings "WEBHOOK_ADDRESS" -}} +spec: + template: + metadata: + annotations: + kubectl.kubernetes.io/default-container: {{ $mainContainerName }} + spec: + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 6 }} + containers: + {{- include "kube_api_rewriter.sidecar_container" (tuple $ctx $settings) | nindent 6 }} + - name: {{ $mainContainerName }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 8 }} + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 8 }} +{{- end -}} + + +{{- define "kube_api_rewriter.service_spec_port_patch_json" -}} + '{{ include "kube_api_rewriter.service_spec_port_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.service_spec_port_patch" -}} +spec: + ports: + - name: {{ include "kube_api_rewriter.webhook_port_name" . }} + port: {{ include "kube_api_rewriter.webhook_port" . }} + protocol: TCP + targetPort: {{ include "kube_api_rewriter.webhook_port_name" . }} +{{- end }} + + +{{- define "kube_api_rewriter.webhook_spec_port_patch_json" -}} + '{{ include "kube_api_rewriter.webhook_spec_port_patch" . | fromYaml | toJson }}' +{{- end }} + +{{- define "kube_api_rewriter.webhook_spec_port_patch" -}} +{{- $webhookNames := list . -}} +{{- if (kindIs "slice" .) -}} +{{- $webhookNames = . -}} +{{- end -}} +webhooks: +{{- range $webhookNames }} +- name: {{ . }} + clientConfig: + service: + port: {{ include "kube_api_rewriter.webhook_port" . }} +{{- end -}} +{{- end -}} diff --git a/templates/kube-api-rewriter/_settings.tpl b/templates/kube-api-rewriter/_settings.tpl new file mode 100644 index 0000000..8f54135 --- /dev/null +++ b/templates/kube-api-rewriter/_settings.tpl @@ -0,0 +1,32 @@ +{{- define "kube_api_rewriter.sidecar_name" -}}proxy{{- end -}} + +{{- define "kube_api_rewriter.webhook_port" -}}24192{{- end -}} + +{{- /* Port name length must be no more than 15 characters. */ -}} +{{- define "kube_api_rewriter.webhook_port_name" -}}webhook-proxy{{- end -}} + +{{- define "kube_api_rewriter.pprof_port" -}}8129{{- end -}} + +{{- define "kube_api_rewriter.env" -}} +- name: LOG_LEVEL + value: {{ include "moduleLogLevel" . }} +{{- if eq (include "moduleLogLevel" .) "debug" }} +- name: PPROF_BIND_ADDRESS + value: ":{{ include "kube_api_rewriter.pprof_port" . }}" +{{- end }} +{{- end -}} + +{{- define "kube_api_rewriter.resources" -}} +cpu: 100m +memory: 30Mi +{{- end -}} + +{{- define "kube_api_rewriter.vpa_container_policy" -}} +- containerName: proxy + minAllowed: + cpu: 10m + memory: 30Mi + maxAllowed: + cpu: 20m + memory: 60Mi +{{- end -}} diff --git a/templates/kube-api-rewriter/_sidecar_helpers.tpl b/templates/kube-api-rewriter/_sidecar_helpers.tpl new file mode 100644 index 0000000..2ae379c --- /dev/null +++ b/templates/kube-api-rewriter/_sidecar_helpers.tpl @@ -0,0 +1,199 @@ +{{- /* Helpers to add kube-api-rewriter sidecar container to a pod. + +To connect to kube-api-rewriter main controller should has KUBECONFIG env, +volumeMount with kubeconfig, and Pod should has volume with kubeconfig ConfigMap. + +These settings are provided by helpers: + +- kube_api_rewriter.kubeconfig_env defines KUBECONFIG env with file from the + mounted ConfigMap. +- kube_api_rewriter.kubeconfig_volume_mount defines volumeMount for kubeconfig ConfigMap. +- kube_api_rewriter.kubeconfig_volume defines volume with kubeconfig ConfigMap. + +Kube-api-rewriter sidecar should be the first container in the Pod, to +main controller not fail on start. + +Kube-api-rewriter sidecar works in 2 modes: without webhook or with webhook rewriting. + +Sidecar without webhook is the simplest one: + +spec: + template: + spec: + containers: + {{ include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: main-controller + ... + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + ... + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ... + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" | nindent 8 }} + ... + + +Webhook mode requires additional settings: + +- WEBHOOK_ADDRESS - address of the webhook in the main controller +- WEBHOOK_CERT_FILE - path to the webhook certificate file. +- WEBHOOK_KEY_FILE - path to the webhook key file. +- webhookCertsVolumeName - name of the Pod volume with webhook certificates. +- webhookCertsMountPath - path to mount the webhook certificates. + +The assumption here is that main controller has a webhook server and +certificates are already mounted in the Pod, so kube-api-rewriter +can use certificates from that volume to impersonate the webhook server. + +Example of adding kube-api-rewriter to the Deployment: + +spec: + template: + spec: + containers: + {{- $rewriterSettings := dict }} + {{- $_ := set $rewriterSettings "WEBHOOK_ADDRESS" "https://127.0.0.1:6443" }} + {{- $_ := set $rewriterSettings "WEBHOOK_CERT_FILE" "/etc/webhook-certificates/tls.crt" }} + {{- $_ := set $rewriterSettings "WEBHOOK_KEY_FILE" "/etc/webhook-certificates/tls.key" }} + {{- $_ := set $rewriterSettings "webhookCertsVolumeName" "webhook-certs" }} + {{- $_ := set $rewriterSettings "webhookCertsMountPath" "/etc/webhook-certificates" }} + {{- include "kube_api_rewriter.sidecar_container" (tuple . $rewriterSettings) | nindent 6 }} + - name: main-controller + ... + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + ... + ports: + - containerPort: 6443 # Goes to the WEBHOOK_ADDRESS + name: webhooks + protocol: TCP + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + - name: webhook-certs + mountPath: /etc/webhook-certificates # Goes to the webhookCertsMountPath + readOnly: true + ... + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" | nindent 8 }} + - name: webhook-certs # Name of the existing volume goes to the webhookCertsVolumeName. + secret: + optional: true + secretName: webhook-certs + ... + + */ -}} + +{{- define "kube_api_rewriter.image" -}} +{{- include "helm_lib_module_image" (list . "kubeApiRewriter") | toJson -}} +{{- end -}} + + +{{- define "kube_api_rewriter.kubeconfig_env" -}} +- name: KUBECONFIG + value: /kubeconfig.local/kube-api-rewriter.kubeconfig +{{- end }} + +{{- define "kube_api_rewriter.kubeconfig_volume" -}} +- name: kube-api-rewriter-kubeconfig + configMap: + defaultMode: 0644 + name: kube-api-rewriter-kubeconfig +{{- end }} + +{{- define "kube_api_rewriter.kubeconfig_volume_mount" -}} +- name: kube-api-rewriter-kubeconfig + mountPath: /kubeconfig.local +{{- end }} + + +{{- define "kube_api_rewriter.webhook_volume_mount" -}} +{{- $volumeName := index . 0 -}} +{{- $mountPath := index . 1 -}} +- mountPath: {{ $mountPath }} + name: {{ $volumeName }} + readOnly: true +{{- end }} + +{{- define "kube_api_rewriter.webhook_container_port" -}} +- containerPort: {{ include "kube_api_rewriter.webhook_port" . }} + name: {{ include "kube_api_rewriter.webhook_port_name" . }} + protocol: TCP +{{- end }} + +{{- /* Container port for the pprof server */ -}} +{{- define "kube_api_rewriter.pprof_container_port" -}} +- containerPort: {{ include "kube_api_rewriter.pprof_port" . }} + name: pprof + protocol: TCP +{{- end }} + +{{- /* Sidecar container spec with kube-api-rewriter */ -}} +{{- /* Usage without the webhook proxy: {{ include kube_api_rewriter.sidecar_container . }} */ -}} +{{- /* Usage with the webhook: {{ include kube_api_rewriter.sidecar_container (tuple . $webhookSettings) }} */ -}} +{{- define "kube_api_rewriter.sidecar_container" -}} + {{- $ctx := . -}} + {{- $settings := dict -}} + {{- if (kindIs "slice" .) -}} + {{- $ctx = index . 0 -}} + {{- if ge (len .) 2 -}} + {{- $settings = index . 1 -}} + {{- end -}} + {{- end -}} + {{- $isWebhook := hasKey $settings "WEBHOOK_ADDRESS" -}} +- name: {{ include "kube_api_rewriter.sidecar_name" $ctx }} + image: {{ include "kube_api_rewriter.image" $ctx }} + imagePullPolicy: IfNotPresent + env: + {{- if $isWebhook }} + - name: WEBHOOK_ADDRESS + value: "{{ $settings.WEBHOOK_ADDRESS }}" + - name: WEBHOOK_CERT_FILE + value: "{{ $settings.WEBHOOK_CERT_FILE }}" + - name: WEBHOOK_KEY_FILE + value: "{{ $settings.WEBHOOK_KEY_FILE }}" + {{- end }} + - name: MONITORING_BIND_ADDRESS + value: "127.0.0.1:9090" + {{- include "kube_api_rewriter.env" $ctx | nindent 4 }} + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 6 }} + {{- if not ( $ctx.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "kube_api_rewriter.resources" . | nindent 6 }} + {{- end }} + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + livenessProbe: + httpGet: + path: /proxy/healthz + port: 8082 + scheme: HTTPS + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: /proxy/readyz + port: 8082 + scheme: HTTPS + initialDelaySeconds: 10 + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + {{- if $isWebhook }} + volumeMounts: + {{- include "kube_api_rewriter.webhook_volume_mount" (tuple $settings.webhookCertsVolumeName $settings.webhookCertsMountPath) | nindent 4 }} + {{- end }} + ports: + {{- if eq (include "moduleLogLevel" $ctx) "debug" }} + {{- include "kube_api_rewriter.pprof_container_port" . | nindent 4 }} + {{- end }} + {{- if $isWebhook -}} + {{- include "kube_api_rewriter.webhook_container_port" .| nindent 4 }} + {{- end -}} +{{- end -}} diff --git a/templates/kube-api-rewriter/cm-kubeconfig-local.yaml b/templates/kube-api-rewriter/cm-kubeconfig-local.yaml new file mode 100644 index 0000000..966a348 --- /dev/null +++ b/templates/kube-api-rewriter/cm-kubeconfig-local.yaml @@ -0,0 +1,20 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: kube-api-rewriter-kubeconfig + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} +data: + kube-api-rewriter.kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + server: http://127.0.0.1:23915 + name: kube-api-rewriter + contexts: + - context: + cluster: kube-api-rewriter + name: kube-api-rewriter + current-context: kube-api-rewriter diff --git a/templates/kube-rbac-proxy/_helpers.tpl b/templates/kube-rbac-proxy/_helpers.tpl new file mode 100644 index 0000000..ee21a1a --- /dev/null +++ b/templates/kube-rbac-proxy/_helpers.tpl @@ -0,0 +1,92 @@ +{{- define "kube_rbac_proxy.sidecar_container" -}} +{{- $ctx := index . 0 }} +{{- $settings := index . 1 }} +- name: {{ $settings.containerName | default "kube-rbac-proxy" }} + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" $ctx | nindent 2 }} + {{- if eq $settings.runAsUserNobody true }} + runAsNonRoot: true + runAsUser: 65534 + runAsGroup: 65534 + {{- end }} + image: {{ include "helm_lib_module_common_image" (list $ctx "kubeRbacProxy") }} + imagePullPolicy: IfNotPresent + terminationMessagePath: /dev/termination-log + terminationMessagePolicy: File + args: + - "--secure-listen-address=$(KUBE_RBAC_PROXY_LISTEN_ADDRESS):{{ $settings.listenPort | default "8082" }}" + - "--v={{ $settings.logLevel | default "2" }}" + - "--logtostderr=true" + - "--stale-cache-interval={{ $settings.staleCacheInterval | default "1h30m" }}" + {{- if hasKey $settings "ignorePaths" }} + - "--ignore-paths={{ $settings.ignorePaths }}" + {{- end }} + env: + - name: KUBE_RBAC_PROXY_LISTEN_ADDRESS + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: KUBE_RBAC_PROXY_CONFIG + value: | + excludePaths: + - {{ $settings.excludePath | default "/config" }} + upstreams: + {{- range $settings.upstreams }} + - upstream: {{ .upstream }} + path: {{ .path }} + authorization: + resourceAttributes: + namespace: {{ .namespace | default "d8-operator-helm" }} + apiGroup: {{ .apiGroup | default "apps" }} + apiVersion: {{ .apiVersion | default "v1" }} + resource: {{ .resource | default "deployments" }} + subresource: {{ .subresource | default "prometheus-metrics" }} + name: {{ .name }} + {{- end }} + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" $ctx | nindent 6 }} + {{- if not ( $ctx.Values.global.enabledModules | has "vertical-pod-autoscaler") }} + {{- include "helm_lib_container_kube_rbac_proxy_resources" $ctx | nindent 6 }} + {{- end }} + ports: + - containerPort: {{ $settings.listenPort | default "8082" }} + name: {{ $settings.portName | default "https-metrics" }} + protocol: TCP + livenessProbe: + tcpSocket: + port: {{ $settings.portName | default "https-metrics" }} + initialDelaySeconds: 10 + readinessProbe: + tcpSocket: + port: {{ $settings.portName | default "https-metrics" }} + initialDelaySeconds: 10 +{{- end -}} + +{{- define "kube_rbac_proxy.pod_spec_strategic_patch" -}} +{{- $ctx := index . 0 }} +{{- $settings := index . 1 }} +spec: + template: + spec: + containers: + {{- include "kube_rbac_proxy.sidecar_container" (tuple $ctx $settings) | nindent 6 }} +{{- end }} + +{{- define "kube_rbac_proxy.image" -}} +{{- include "helm_lib_module_common_image" (list . "kubeRbacProxy") -}} +{{- end -}} + +{{- define "kube_rbac_proxy.vpa_container_policy" -}} +- containerName: {{ $.containerName | default "kube-rbac-proxy" }} + minAllowed: + cpu: 10m + memory: 15Mi + maxAllowed: + cpu: 20m + memory: 30Mi +{{- end -}} + +{{- define "kube_rbac_proxy.pod_spec_strategic_patch_json" -}} + '{{ include "kube_rbac_proxy.pod_spec_strategic_patch" . | fromYaml | toJson }}' +{{- end }} diff --git a/templates/namespace.yaml b/templates/namespace.yaml new file mode 100644 index 0000000..c9603eb --- /dev/null +++ b/templates/namespace.yaml @@ -0,0 +1,8 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + {{- include "helm_lib_module_labels" (list . (dict "prometheus.deckhouse.io/rules-watcher-enabled" "true")) | nindent 2 }} + name: d8-{{ .Chart.Name }} +--- +{{- include "helm_lib_kube_rbac_proxy_ca_certificate" (list . (printf "d8-%s" .Chart.Name)) }} diff --git a/templates/nelm-source-controller/_helpers.tpl b/templates/nelm-source-controller/_helpers.tpl new file mode 100644 index 0000000..e0b5dc4 --- /dev/null +++ b/templates/nelm-source-controller/_helpers.tpl @@ -0,0 +1,8 @@ +{{- define "nelm-source-controller.envs" -}} +- name: RUNTIME_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace +- name: TUF_ROOT + value: /tmp/.sigstore +{{- end }} diff --git a/templates/nelm-source-controller/deployment.yaml b/templates/nelm-source-controller/deployment.yaml new file mode 100644 index 0000000..f8eef88 --- /dev/null +++ b/templates/nelm-source-controller/deployment.yaml @@ -0,0 +1,143 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "nelm_source_controller_resources" }} +cpu: 50m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: nelm-source-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: nelm-source-controller + minAllowed: + {{- include "nelm_source_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: nelm-source-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + replicas: 1 + strategy: + type: Recreate + revisionHistoryLimit: 2 + selector: + matchLabels: + app: nelm-source-controller + template: + metadata: + labels: + app: nelm-source-controller + annotations: + kubectl.kubernetes.io/default-container: nelm-source-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: nelm-source-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "nelmSourceController") }} + imagePullPolicy: IfNotPresent + args: + - --watch-all-namespaces + - --log-level={{ include "moduleLogLevel" . }} + - --log-encoding=json + - --enable-leader-election + - --storage-path=/data + - --storage-addr=:9091 + - --storage-adv-addr=nelm-source-controller.$(RUNTIME_NAMESPACE).svc.{{ .Values.global.discovery.clusterDomain }} + volumeMounts: + - mountPath: /data + name: data + - mountPath: /tmp + name: tmp + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 9091 + name: controller + protocol: TCP + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + protocol: TCP + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "nelm_source_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + {{- include "nelm-source-controller.envs" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: / + port: controller + scheme: HTTP + initialDelaySeconds: 10 + {{- $kubeRbacProxySettings := dict }} + {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" false }} + {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} + {{- $_ := set $kubeRbacProxySettings "upstreams" (list + (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "nelm-source-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: nelm-source-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "nelm-source-controller")) | nindent 6 }} + volumes: + - emptyDir: {} + name: data + - emptyDir: {} + name: tmp + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/nelm-source-controller/rbac-for-us.yaml b/templates/nelm-source-controller/rbac-for-us.yaml new file mode 100644 index 0000000..a05bf0e --- /dev/null +++ b/templates/nelm-source-controller/rbac-for-us.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:nelm-source-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:nelm-source-controller +subjects: +- kind: ServiceAccount + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:nelm-source-controller +rules: +- apiGroups: + - "" + resources: + - pods + - services + - secrets + - configmaps + verbs: + - get + - create + - update + - delete + - list + - watch + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets + - internalnelmoperatorgitrepositories + - internalnelmoperatorhelmcharts + - internalnelmoperatorhelmrepositories + - internalnelmoperatorocirepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/finalizers + - internalnelmoperatorgitrepositories/finalizers + - internalnelmoperatorhelmcharts/finalizers + - internalnelmoperatorhelmrepositories/finalizers + - internalnelmoperatorocirepositories/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/status + - internalnelmoperatorgitrepositories/status + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorhelmrepositories/status + - internalnelmoperatorocirepositories/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: nelm-source-controller +subjects: +- kind: ServiceAccount + name: nelm-source-controller diff --git a/templates/nelm-source-controller/service-metrics.yaml b/templates/nelm-source-controller/service-metrics.yaml new file mode 100644 index 0000000..dff6d72 --- /dev/null +++ b/templates/nelm-source-controller/service-metrics.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nelm-source-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: nelm-source-controller diff --git a/templates/nelm-source-controller/service-monitor.yaml b/templates/nelm-source-controller/service-monitor.yaml new file mode 100644 index 0000000..6538666 --- /dev/null +++ b/templates/nelm-source-controller/service-monitor.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: nelm-source-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "nelm-source-controller" diff --git a/templates/nelm-source-controller/service.yaml b/templates/nelm-source-controller/service.yaml new file mode 100644 index 0000000..1d1bfa2 --- /dev/null +++ b/templates/nelm-source-controller/service.yaml @@ -0,0 +1,15 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: nelm-source-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "nelm-source-controller")) | nindent 2 }} +spec: + ports: + - name: controller + port: 80 + targetPort: controller + protocol: TCP + selector: + app: nelm-source-controller diff --git a/templates/rbac-to-us.yaml b/templates/rbac-to-us.yaml new file mode 100644 index 0000000..a7e15af --- /dev/null +++ b/templates/rbac-to-us.yaml @@ -0,0 +1,32 @@ +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: access-to-operator-helm + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} +rules: +- apiGroups: ["apps"] + resources: ["deployments/prometheus-metrics"] + resourceNames: ["helm-controller", "nelm-source-controller", "kube-api-rewriter"] + verbs: ["get"] + +{{- if (.Values.global.enabledModules | has "prometheus") }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: access-to-virtualization + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: access-to-operator-helm +subjects: +- kind: User + name: d8-monitoring:scraper +- kind: ServiceAccount + name: prometheus + namespace: d8-monitoring +{{- end }} diff --git a/templates/registry-secret.yaml b/templates/registry-secret.yaml new file mode 100644 index 0000000..2001911 --- /dev/null +++ b/templates/registry-secret.yaml @@ -0,0 +1,16 @@ +{{/* Use module specific dockercfg if set. Use global dockercfg if module included as embedded. */}} +{{- $dockercfg := dig "registry" "dockercfg" "" .Values.operatorHelm }} +{{- if eq $dockercfg "" }} +{{/* Workaround to exclude check https://github.com/deckhouse/dmt/pull/236 */}} +{{- $dockercfg = dig "modulesImages" "registry" "dockercfg" "" .Values.global }} +{{- end }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: operator-helm-module-registry + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} +type: kubernetes.io/dockerconfigjson +data: + .dockerconfigjson: {{ $dockercfg | quote }} diff --git a/tmp/mc-operator-helm.yaml b/tmp/mc-operator-helm.yaml new file mode 100644 index 0000000..24275b2 --- /dev/null +++ b/tmp/mc-operator-helm.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleConfig +metadata: + name: operator-helm +spec: + enabled: false + source: operator-helm + version: 1 diff --git a/tmp/modulepulloverride.yaml b/tmp/modulepulloverride.yaml new file mode 100644 index 0000000..9f2f086 --- /dev/null +++ b/tmp/modulepulloverride.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha2 +kind: ModulePullOverride +metadata: + name: operator-helm +spec: + imageTag: mvp + rollback: true + scanInterval: 15s diff --git a/tmp/modulesource.yaml b/tmp/modulesource.yaml new file mode 100644 index 0000000..9bf6aa5 --- /dev/null +++ b/tmp/modulesource.yaml @@ -0,0 +1,8 @@ +apiVersion: deckhouse.io/v1alpha1 +kind: ModuleSource +metadata: + name: operator-helm +spec: + registry: + repo: ghcr.io/deckhouse/operator-helm + scheme: HTTPS diff --git a/tools/validation/diff.go b/tools/validation/diff.go new file mode 100644 index 0000000..516388b --- /dev/null +++ b/tools/validation/diff.go @@ -0,0 +1,149 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "fmt" + "io" + "regexp" + "strings" +) + +type DiffInfo struct { + Files []*DiffFileInfo +} + +func NewDiffInfo() *DiffInfo { + return &DiffInfo{ + Files: make([]*DiffFileInfo, 0), + } +} + +func (d *DiffInfo) Dump() string { + res := "" + for _, info := range d.Files { + res += fmt.Sprintf("%s -> %s, lines: %d\n", info.OldFileName, info.NewFileName, len(info.Lines)) + } + res += fmt.Sprintf("files: %d\n", len(d.Files)) + return res +} + +type DiffFileInfo struct { + NewFileName string + OldFileName string + Lines []string +} + +func (d *DiffFileInfo) IsAdded() bool { + return d.OldFileName == "/dev/null" +} + +func (d *DiffFileInfo) IsDeleted() bool { + return d.NewFileName == "/dev/null" +} + +func (d *DiffFileInfo) IsModified() bool { + return d.OldFileName != "/dev/null" && d.NewFileName != "/dev/null" && d.HasContent() +} + +func (d *DiffFileInfo) HasContent() bool { + return len(d.Lines) > 0 +} + +func (d *DiffFileInfo) NewLines() []string { + res := make([]string, 0) + for _, l := range d.Lines { + if strings.HasPrefix(l, "+") { + res = append(res, strings.TrimPrefix(l, "+")) + } + } + return res +} + +func NewDiffFileInfo() *DiffFileInfo { + return &DiffFileInfo{ + Lines: make([]string, 0), + } +} + +var diffStartRe = regexp.MustCompile(`^diff --git a/(.*) b/(.*)$`) +var oldFileNameRe = regexp.MustCompile(`^--- (/dev/null|a/(.*))$`) +var newFileNameRe = regexp.MustCompile(`^\+\+\+ (/dev/null|b/(.*))$`) +var endMetadataRe = regexp.MustCompile(`^@@[\-+ \d,]+@@(.*)$`) + +func ParseDiffOutput(r io.Reader) (*DiffInfo, error) { + res := NewDiffInfo() + tmp := NewDiffFileInfo() + firstLine := true + scanner := bufio.NewScanner(r) + metadataBlock := false + for scanner.Scan() { + text := scanner.Text() + + if diffStartRe.MatchString(text) { + if firstLine { + firstLine = false + } else { + // Append diffFileInfo when all lines are gathered and new diffFIleInfo is detected. + res.Files = append(res.Files, tmp) + tmp = NewDiffFileInfo() + } + metadataBlock = true + continue + } + + matches := newFileNameRe.FindStringSubmatch(text) + if len(matches) > 1 { + if matches[1] == "/dev/null" { + tmp.NewFileName = matches[1] + } else { + tmp.NewFileName = matches[2] + } + continue + } + + matches = oldFileNameRe.FindStringSubmatch(text) + if len(matches) > 1 { + if matches[1] == "/dev/null" { + tmp.OldFileName = matches[1] + } else { + tmp.OldFileName = matches[2] + } + continue + } + + if metadataBlock { + matches = endMetadataRe.FindStringSubmatch(text) + if len(matches) > 1 { + tmp.Lines = append(tmp.Lines, matches[1]) + metadataBlock = false + continue + } + } + + if !metadataBlock { + tmp.Lines = append(tmp.Lines, text) + } + } + // Push last diff info. + if tmp != nil { + res.Files = append(res.Files, tmp) + } + + return res, nil +} diff --git a/tools/validation/doc_changes.go b/tools/validation/doc_changes.go new file mode 100644 index 0000000..08c9c2c --- /dev/null +++ b/tools/validation/doc_changes.go @@ -0,0 +1,143 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "os" + "regexp" + "strings" +) + +var ( + resourceFileRe = regexp.MustCompile(`openapi/config-values.y[a]?ml$|crds/.+.y[a]?ml$`) + docFileRe = regexp.MustCompile(`\.md$`) + + excludeFileRe = regexp.MustCompile("crds/embedded/.+.y[a]?ml$") +) + +func RunDocChangesValidation(info *DiffInfo) (exitCode int) { + fmt.Printf("Run 'doc changes' validation ...\n") + + if len(info.Files) == 0 { + fmt.Printf("Nothing to validate, diff is empty\n") + return 0 + } + + exitCode = 0 + msgs := NewMessages() + for _, fileInfo := range info.Files { + if !fileInfo.HasContent() { + continue + } + + fileName := fileInfo.NewFileName + + if strings.Contains(fileName, "testdata") { + msgs.Add(NewSkip(fileName, "")) + continue + } + + if docFileRe.MatchString(fileName) { + msgs.Add(checkDocFile(fileName, info)) + continue + } + + if resourceFileRe.MatchString(fileName) && !excludeFileRe.MatchString(fileName) { + msgs.Add(checkResourceFile(fileName, info)) + continue + } + + msgs.Add(NewSkip(fileName, "")) + } + msgs.PrintReport() + + if msgs.CountErrors() > 0 { + exitCode = 1 + } + + return exitCode +} + +var possibleDocRootsRe = regexp.MustCompile(`docs/|docs/documentation`) +var docsDirAllowedFileRe = regexp.MustCompile(`docs/(CONFIGURATION|CR|FAQ|README|ADMIN_GUIDE|USER_GUIDE|CHARACTERISTICS_DESCRIPTION|INSTALL|RELEASE_NOTES)(\.ru)?.md`) +var docsDirFileRe = regexp.MustCompile(`docs/[^/]+.md`) + +func checkDocFile(fName string, diffInfo *DiffInfo) (msg Message) { + if !possibleDocRootsRe.MatchString(fName) { + return NewSkip(fName, "") + } + + if docsDirFileRe.MatchString(fName) && !docsDirAllowedFileRe.MatchString(fName) { + return NewError( + fName, + "name is not allowed", + `Rename this file or move it, for example, into 'internal' folder. +Only following file names are allowed in the module '/docs/' directory: + CLUSTER_CONFIGURATION.md + CONFIGURATION.md + CR.md + FAQ.md + README.md + RELEASE_NOTES.md + ADMIN_GUIDE.md + USER_GUIDE.md + CHARACTERISTICS_DESCRIPTION.md +(also their Russian versions ended with '.ru.md')`, + ) + } + + // Check if documentation for other language file is also modified. + var otherFileName = fName + if strings.HasSuffix(fName, `.ru.md`) { + otherFileName = strings.TrimSuffix(fName, ".ru.md") + ".md" + } else { + otherFileName = strings.TrimSuffix(fName, ".md") + ".ru.md" + } + return checkRelatedFileExists(fName, otherFileName, diffInfo) +} + +var docRuResourceRe = regexp.MustCompile(`doc-ru-.+.y[a]?ml$`) +var notDocRuResourceRe = regexp.MustCompile(`([^/]+\.y[a]?ml)$`) + +// Check if resource for other language is also modified. +func checkResourceFile(fName string, diffInfo *DiffInfo) (msg Message) { + otherFileName := fName + if docRuResourceRe.MatchString(fName) { + otherFileName = strings.Replace(fName, "doc-ru-", "", 1) + } else { + otherFileName = notDocRuResourceRe.ReplaceAllString(fName, `doc-ru-$1`) + } + return checkRelatedFileExists(fName, otherFileName, diffInfo) +} + +func checkRelatedFileExists(origName string, otherName string, diffInfo *DiffInfo) Message { + file, err := os.Open(otherName) + if err != nil { + return NewError(origName, "related is absent", fmt.Sprintf(`Documentation or resource file is changed +while related language file '%s' is absent.`, otherName)) + } + defer file.Close() + + for _, fileInfo := range diffInfo.Files { + if fileInfo.NewFileName == otherName { + return NewOK(origName) + } + } + return NewError(origName, "related not changed", fmt.Sprintf(`Documentation or resource file is changed +while related language file '%s' is not changed`, otherName)) +} diff --git a/tools/validation/go.mod b/tools/validation/go.mod new file mode 100644 index 0000000..3102e06 --- /dev/null +++ b/tools/validation/go.mod @@ -0,0 +1,3 @@ +module validation + +go 1.21.4 diff --git a/tools/validation/main.go b/tools/validation/main.go new file mode 100644 index 0000000..4829162 --- /dev/null +++ b/tools/validation/main.go @@ -0,0 +1,97 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "flag" + "fmt" + "os" + "os/exec" +) + +func main() { + var validationType string + flag.StringVar(&validationType, "type", "", "Validation type: cyrillic or doc-changes.") + var patchFile string + flag.StringVar(&patchFile, "file", "", "Patch file. git diff is executed if not passed.") + var title string + flag.StringVar(&title, "title", "", "Title string to check for cyrillic letters.") + var description string + flag.StringVar(&description, "description", "", "Description string to check for cyrillic letters.") + flag.Parse() + + var diffInfo *DiffInfo + var err error + if patchFile != "" { + // Parse file content. + diffInfo, err = readFile(patchFile) + if err != nil { + fmt.Printf("Read file '%s': %v", patchFile, err) + os.Exit(1) + } + } else { + // Parse 'git diff' output. + fmt.Printf("Run git diff ...\n") + diffInfo, err = executeGitDiff() + if err != nil { + fmt.Printf("Execute git diff: %v", err) + os.Exit(1) + } + } + + exitCode := 0 + switch validationType { + case "no-cyrillic": + exitCode = RunNoCyrillicValidation(diffInfo, title, description) + case "doc-changes": + exitCode = RunDocChangesValidation(diffInfo) + case "dump": + fmt.Printf("%s\n", diffInfo.Dump()) + default: + fmt.Printf("Unknown validation type '%s'\n", validationType) + os.Exit(2) + } + + if exitCode == 0 { + fmt.Printf("Validation successful.\n") + } else { + fmt.Printf("Validation failed.\n") + } + os.Exit(exitCode) +} + +func readFile(fName string) (*DiffInfo, error) { + content, err := os.ReadFile(fName) + if err != nil { + return nil, err + } + + br := bytes.NewReader(content) + return ParseDiffOutput(br) +} + +func executeGitDiff() (*DiffInfo, error) { + gitCmd := exec.Command("git", "diff", "origin/main...", "-w", "--ignore-blank-lines") + out, err := gitCmd.Output() + if err != nil { + return nil, err + } + + br := bytes.NewReader(out) + return ParseDiffOutput(br) +} diff --git a/tools/validation/messages.go b/tools/validation/messages.go new file mode 100644 index 0000000..6cd933a --- /dev/null +++ b/tools/validation/messages.go @@ -0,0 +1,176 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" +) + +const OKType = "OK" +const SkipType = "Skip" +const ErrorType = "ERROR" + +type Message struct { + Type string + FileName string + Message string + Details string +} + +func NewOK(fileName string) Message { + return Message{ + Type: OKType, + FileName: fileName, + } +} + +func NewSkip(fileName string, msg string) Message { + return Message{ + Type: SkipType, + FileName: fileName, + Message: msg, + } +} + +func NewError(fileName string, msg string, details string) Message { + return Message{ + Type: ErrorType, + FileName: fileName, + Message: msg, + Details: details, + } +} + +func (msg Message) Format() string { + res := "" + if msg.Message == "" { + res += fmt.Sprintf(" * %s ... %s", msg.FileName, msg.Type) + } else { + res += fmt.Sprintf(" * %s ... %s: %s", msg.FileName, msg.Type, msg.Message) + } + if msg.Details != "" { + res += "\n" + indentTextBlock(msg.Details, 6) + } + return res +} + +func (msg Message) IsError() bool { + return msg.Type == ErrorType +} + +func (msg Message) IsSkip() bool { + return msg.Type == SkipType +} + +func (msg Message) IsOK() bool { + return msg.Type == OKType +} + +type Messages struct { + messages []Message +} + +func NewMessages() *Messages { + return &Messages{ + messages: make([]Message, 0), + } +} + +func (m *Messages) Add(msg Message) { + m.messages = append(m.messages, msg) +} + +func (m *Messages) Join(msgs *Messages) { + if msgs == nil { + return + } + for _, message := range msgs.messages { + m.Add(message) + } +} + +func (m *Messages) CountOK() int { + res := 0 + for _, msg := range m.messages { + if msg.IsOK() { + res++ + } + } + return res +} + +func (m *Messages) CountSkip() int { + res := 0 + for _, msg := range m.messages { + if msg.IsSkip() { + res++ + } + } + return res +} + +func (m *Messages) CountErrors() int { + res := 0 + for _, msg := range m.messages { + if msg.IsError() { + res++ + } + } + return res +} + +func (m *Messages) PrintReport() { + if m.CountSkip() > 0 { + fmt.Println("Skipped:") + for _, msg := range m.messages { + if msg.IsSkip() { + fmt.Println(msg.Format()) + } + } + } + if m.CountOK() > 0 { + fmt.Println("OK:") + for _, msg := range m.messages { + if msg.IsOK() { + fmt.Println(msg.Format()) + } + } + } + if m.CountErrors() > 0 { + fmt.Println("ERRORS:") + for _, msg := range m.messages { + if msg.IsError() { + fmt.Println(msg.Format()) + } + } + } +} + +func indentTextBlock(msg string, n int) string { + lines := strings.Split(msg, "\n") + var b strings.Builder + for i, line := range lines { + // leading newline and newlines between lines + if i > 0 { + b.WriteString("\n") + } + b.WriteString(strings.Repeat(" ", n)) + b.WriteString(line) + } + return b.String() +} diff --git a/tools/validation/no_cyrillic.go b/tools/validation/no_cyrillic.go new file mode 100644 index 0000000..d41c65a --- /dev/null +++ b/tools/validation/no_cyrillic.go @@ -0,0 +1,160 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "regexp" + "strings" +) + +var skipDocRe = regexp.MustCompile(`doc-ru-.+\.y[a]?ml$|\.ru\.md$`) +var skipI18NRe = regexp.MustCompile(`/i18n/`) +var skipSelfRe = regexp.MustCompile(`no_cyrillic(_test)?.go$`) + +func RunNoCyrillicValidation(info *DiffInfo, title string, description string) (exitCode int) { + fmt.Printf("Run 'no cyrillic' validation ...\n") + + exitCode = 0 + if title != "" { + fmt.Printf("Check title ... ") + msg, hasCyr := checkCyrillicLetters(title) + if hasCyr { + fmt.Printf("ERROR\n%s\n", msg) + exitCode = 1 + } else { + fmt.Printf("OK\n") + } + } + if description != "" { + // Here put cyrillic char -> C + fmt.Printf("Check description Сахар... ") + msg, hasCyr := checkCyrillicLetters(description) + if hasCyr { + fmt.Printf("ERROR\n%s\n", msg) + exitCode = 1 + } else { + fmt.Printf("OK\n") + } + } + // Some fishka + fmt.Printf("Check new and updated lines ... ") + if len(info.Files) == 0 { + fmt.Printf("OK, diff is empty\n") + } else { + fmt.Println("") + + msgs := NewMessages() + + //hasErrors := false + for _, fileInfo := range info.Files { + if !fileInfo.HasContent() { + continue + } + // Check only added or modified files + if !(fileInfo.IsAdded() || fileInfo.IsModified()) { + continue + } + + fileName := fileInfo.NewFileName + + if skipDocRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "documentation")) + continue + } + + if skipI18NRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "translation file")) + continue + } + + if skipSelfRe.MatchString(fileName) { + msgs.Add(NewSkip(fileName, "self")) + continue + } + + // Get added or modified lines + newLines := fileInfo.NewLines() + if len(newLines) == 0 { + msgs.Add(NewSkip(fileName, "no lines added")) + continue + } + + cyrMsg, hasCyr := checkCyrillicLettersInArray(newLines) + if hasCyr { + msgs.Add(NewError(fileName, "should not contain Cyrillic letters", cyrMsg)) + continue + } + + msgs.Add(NewOK(fileName)) + } + + msgs.PrintReport() + + if msgs.CountErrors() > 0 { + exitCode = 1 + } + } + + return exitCode +} + +var cyrRe = regexp.MustCompile(`[А-Яа-яЁё]+`) +var cyrPointerRe = regexp.MustCompile(`[А-Яа-яЁё]`) +var cyrFillerRe = regexp.MustCompile(`[^А-Яа-яЁё]`) + +func checkCyrillicLetters(in string) (string, bool) { + if strings.Contains(in, "\n") { + return checkCyrillicLettersInArray(strings.Split(in, "\n")) + } + return checkCyrillicLettersInString(in) +} + +// checkCyrillicLettersInString returns a fancy message if input string contains Cyrillic letters. +func checkCyrillicLettersInString(line string) (string, bool) { + if !cyrRe.MatchString(line) { + return "", false + } + + // Replace all tabs with spaces to prevent shifted cursor. + line = strings.Replace(line, "\t", " ", -1) + + // Make string with pointers to Cyrillic letters so user can detect hidden letters. + cursor := cyrFillerRe.ReplaceAllString(line, "-") + cursor = cyrPointerRe.ReplaceAllString(cursor, "^") + cursor = strings.TrimRight(cursor, "-") + + const formatPrefix = " " + + return formatPrefix + line + "\n" + formatPrefix + cursor, true +} + +// checkCyrillicLettersInArray returns a fancy message for each string in array that contains Cyrillic letters. +func checkCyrillicLettersInArray(lines []string) (string, bool) { + res := make([]string, 0) + + hasCyr := false + for _, line := range lines { + msg, has := checkCyrillicLettersInString(line) + if has { + hasCyr = true + res = append(res, msg) + } + } + + return strings.Join(res, "\n"), hasCyr +} diff --git a/tools/validation/no_cyrillic_test.go b/tools/validation/no_cyrillic_test.go new file mode 100644 index 0000000..f239014 --- /dev/null +++ b/tools/validation/no_cyrillic_test.go @@ -0,0 +1,96 @@ +/* +Copyright 2024 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "strings" + "testing" +) + +func Test_found_msg(t *testing.T) { + // Simple check with one Cyrillic letter. + in := "fooБfoo" + expected := ` fooБfoo + ---^` + + actual, has := checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + t.Errorf("Expect '%s', got '%s'", expected, actual) + } + + // No Cyrillic letters. + in = "asdqwe 123456789 !@#$%^&*( ZXCVBNM" + expected = "" + actual, has = checkCyrillicLetters(in) + + if has { + t.Errorf("Should not detect cyrillic letters in string") + } + + if actual != expected { + t.Errorf("Expect '%s', got '%s'", expected, actual) + } + + // Multiple words with Cyrillic letters. + in = "asdqwe Там на qw q cheсk tеst qwd неведомых qqw" + expected = + " asdqwe Там на qw q cheсk tеst qwd неведомых qqw\n" + + " -------^^^-^^---------^---^-------^^^^^^^^^" + + actual, has = checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + fmt.Printf(" %s\n%s\n", + strings.Repeat("0123456789", len(actual)/2/10+1), + actual) + t.Errorf("Expect \n%s\n, got \n%s\n", expected, actual) + } + + // Multiple messages for string with '\n'. + in = "Lorem ipsum dolor sit amet,\n consectetur adipiscing elit,\n" + + "раскрою перед вами всю \nкартину и разъясню," + + "Ut enim ad minim veniam," + expected = + " раскрою перед вами всю \n" + + " ^^^^^^^-^^^^^-^^^^-^^^\n" + + " картину и разъясню,Ut enim ad minim veniam,\n" + + " ^^^^^^^-^-^^^^^^^^" + + actual, has = checkCyrillicLetters(in) + + if !has { + t.Errorf("Should detect cyrillic letters in string") + } + + if actual != expected { + fmt.Printf(" %s\n%s\n", + strings.Repeat("0123456789", len(actual)/2/10+1), + actual) + t.Errorf("Expect \n%s\n, got \n%s\n", expected, actual) + } + +} diff --git a/werf-giterminism.yaml b/werf-giterminism.yaml new file mode 100644 index 0000000..250f208 --- /dev/null +++ b/werf-giterminism.yaml @@ -0,0 +1,28 @@ +giterminismConfigVersion: 1 +config: + goTemplateRendering: # The rules for the Go-template functions + allowEnvVariables: + - /CI_.+/ + - GOPROXY + - MODULES_MODULE_TAG + - SOURCE_REPO + - SOURCE_REPO_GIT + - MODULE_EDITION + - DISTRO_PACKAGES_PROXY + - SVACE_ENABLED + - SVACE_ANALYZE_HOST + - SVACE_ANALYZE_SSH_USER + - DEBUG_COMPONENT + stapel: + mount: + allowBuildDir: true + allowFromPaths: + - ~/go-pkg-cache + secrets: + allowValueIds: + - SOURCE_REPO + - GOPROXY +helm: + allowUncommittedFiles: + - "Chart.lock" + - "charts/*.tgz" diff --git a/werf.yaml b/werf.yaml new file mode 100644 index 0000000..160b16d --- /dev/null +++ b/werf.yaml @@ -0,0 +1,114 @@ +project: operator-helm +configVersion: 1 +build: + imageSpec: + author: "Deckhouse Kubernetes Platform " + clearHistory: true + config: + keepEssentialWerfLabels: true + removeLabels: + - /.*/ +--- +# Base Images +{{- include "parse_base_images_map" . }} +--- +# Source repo settings +{{- $_ := set . "SOURCE_REPO" (env "SOURCE_REPO" "https://github.com") }} + +{{- $_ := set . "SOURCE_REPO_GIT" (env "SOURCE_REPO_GIT" "https://github.com") }} + +# Define packages proxy settings +{{- $_ := set . "DistroPackagesProxy" (env "DISTRO_PACKAGES_PROXY" "") }} + + +# svace analyze toggler +{{- $_ := set . "SVACE_ENABLED" (env "SVACE_ENABLED" "false") }} +{{- $_ := set . "SVACE_ANALYZE_HOST" (env "SVACE_ANALYZE_HOST" "example.host") }} +{{- $_ := set . "SVACE_ANALYZE_SSH_USER" (env "SVACE_ANALYZE_SSH_USER" "user") }} + +{{- $_ := set . "ImagesIDList" list }} + +{{- range $path, $content := .Files.Glob ".werf/*.yaml" }} + {{- tpl $content $ }} +{{- end }} +--- +image: images-digests +fromImage: builder/alpine +dependencies: + {{- range $ImageID := $.ImagesIDList }} + {{- $ImageNameCamel := $ImageID | splitList "/" | last | camelcase | untitle }} +- image: {{ $ImageID }} + before: setup + imports: + - type: ImageDigest + targetEnv: MODULE_IMAGE_DIGEST_{{ $ImageNameCamel }} + {{- end }} +shell: + beforeInstall: + - apk add --no-cache jq + setup: + - | + env | grep MODULE_IMAGE_DIGEST | jq -Rn ' + reduce inputs as $i ( + {}; + . * ( + $i | ltrimstr("MODULE_IMAGE_DIGEST_") | sub("=";"_") | + split("_") as [$imageName, $digest] | + {($imageName): $digest} + ) + ) + ' > /images_digests.json + cat images_digests.json +--- +image: bundle +fromImage: builder/scratch +import: +- image: prepare-bundle + add: /prep-bundle + to: / + after: setup +--- +image: prepare-bundle +fromImage: builder/alpine +import: +- image: images-digests + add: / + to: /prep-bundle + after: setup + includePaths: + - images_digests.json +# - image: go-hooks-artifact +# add: /go-hooks +# to: /prep-bundle/hooks/go +# after: setup +git: + - add: / + to: /prep-bundle + stageDependencies: + install: + - '**/*' + includePaths: + - charts + - crds + - build/components + - docs + - openapi + - monitoring + - templates + - Chart.yaml + - module.yaml + - .helmignore + excludePaths: + - build/components/README.md + - docs/images/*.drawio + - docs/images/*.sh + - docs/internal +shell: + install: + - ls -la /prep-bundle +--- +image: release-channel-version +fromImage: builder/scratch +shell: + install: + - echo '{"version":"{{ env "MODULES_MODULE_TAG" "dev" }}"}' > version.json diff --git a/werf_cleanup.yaml b/werf_cleanup.yaml new file mode 100644 index 0000000..9ca7769 --- /dev/null +++ b/werf_cleanup.yaml @@ -0,0 +1,18 @@ +configVersion: 1 +project: operator-helm +cleanup: + keepPolicies: + - references: + tag: /.*/ + limit: + in: 72h + - references: + branch: /.*/ + limit: + in: 168h # keep dev images build during last week which not main|pre-alpha + - references: + branch: /main|release-[0-9]+.*/ + limit: + last: 5 # keep 5 images for branches release-* and main + imagesPerReference: + last: 1 From 2940b720618eabdddfd5c3f26eb8a4ef18debbf9 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Fri, 20 Feb 2026 10:54:20 +0300 Subject: [PATCH 02/26] refactor: adopt kube-api-rewriter to operator-helm use-cases Signed-off-by: Ilya Drey --- images/kube-api-rewriter/STRUCTURE.md | 450 +--------- images/kube-api-rewriter/Taskfile.dist.yaml | 7 - .../cmd/kube-api-rewriter/main.go | 4 +- images/kube-api-rewriter/go.mod | 41 +- images/kube-api-rewriter/go.sum | 156 ++-- images/kube-api-rewriter/mount-points.yaml | 8 +- .../pkg/kubevirt/kubevirt_rules.go | 698 ---------------- .../pkg/operatornelm/operatornelm_rules.go | 158 ++++ .../operatornelm_rules_test.go} | 8 +- images/kube-api-rewriter/pkg/proxy/handler.go | 66 +- .../pkg/proxy/handler_test.go | 778 ------------------ .../pkg/proxy/stream_handler.go | 2 +- .../pkg/rewriter/resource.go | 4 + .../pkg/rewriter/rule_rewriter.go | 5 + .../kube-api-rewriter/pkg/rewriter/rules.go | 33 + .../pkg/rewriter/source_ref.go | 109 +++ .../pkg/rewriter/source_ref_test.go | 217 +++++ images/kube-api-rewriter/werf.inc.yaml | 4 +- 18 files changed, 651 insertions(+), 2097 deletions(-) delete mode 100644 images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go create mode 100644 images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go rename images/kube-api-rewriter/pkg/{kubevirt/kubevirt_rules_test.go => operatornelm/operatornelm_rules_test.go} (77%) delete mode 100644 images/kube-api-rewriter/pkg/proxy/handler_test.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/source_ref.go create mode 100644 images/kube-api-rewriter/pkg/rewriter/source_ref_test.go diff --git a/images/kube-api-rewriter/STRUCTURE.md b/images/kube-api-rewriter/STRUCTURE.md index bdbf4a2..a3c6033 100644 --- a/images/kube-api-rewriter/STRUCTURE.md +++ b/images/kube-api-rewriter/STRUCTURE.md @@ -1,451 +1,3 @@ # kube-api-rewriter structure -The idea of the rewriter proxy is simple: make controller connect to the local -proxy in the sidecar, so proxy will pass requests to real Kubernetes API Server. -Proxy may rewrite JSON payloads for different purposes, e.g. resources renaming. - -Kube-api-rewriter contains 2 proxy instances: -- "api" proxy to handle usual API requests from the proxied controller to the Kubernetes API Server. -- "webhook" proxy to handle webhook requests from the Kubernetes API Server to the proxied controller. - - -Example setup: rename resources for Kubevirt. -```mermaid -%%{init: {"flowchart": {"htmlLabels": false}} }%% -flowchart TB - NoProxy-.->WithProxy - - subgraph NoProxy ["`**Original Kubevirt setup**`"] - direction TB - - subgraph np-virt-operator-deploy ["`Deploy/virt-operator`"] - np-virt-operator("`container - name: virt-operator`") - end - - subgraph np-virt-controller-deploy ["`Deploy/virt-controller`"] - np-virt-controller("`container - name: virt-controller`") - end - - np-kube-api["`Kubernetes API Server - with resources in apiGroup - *.kubevirt.io*`"] - - np-virt-operator <-- "Original resources - in API calls" --> np-kube-api - np-virt-controller <-- "Original resources - in API calls" --> np-kube-api - end - subgraph WithProxy ["`**Kubevirt with proxy**`"] - direction TB - - subgraph p-virt-operator-deploy ["`Deploy/virt-operator`"] - p-virt-operator("`container - name: virt-operator`") - p-virt-operator-proxy{{"container - name: proxy"}} - p-virt-operator -- "Original resources - in API calls" --> p-virt-operator-proxy - p-virt-operator-proxy -- "Restored resources - in API responses" --> p-virt-operator - end - - subgraph p-virt-controller-deploy ["`Deploy/virt-controller`"] - p-virt-controller("`container - name: virt-controller`") - p-virt-controller-proxy{{"container - name: proxy"}} - p-virt-controller -- "Original resources -in API calls" --> p-virt-controller-proxy - p-virt-controller-proxy -- "Restored resources - in API responses" --> p-virt-controller - end - - p-kube-api["`Kubernetes API Server - with resources in apiGroup - *.x.virtualization.deckhouse.io*`"] - - p-virt-operator-proxy <-- "Renamed resources in - API calls" --> p-kube-api - p-virt-controller-proxy <-- "Renamed resources in - API calls" --> p-kube-api - end -``` - -All DVP components: -```mermaid -%%{init: {"flowchart": {"htmlLabels": false}} }%% -flowchart - subgraph kubevirt ["Kubevirt"] - subgraph virt-operator-deploy ["`Deploy/virt-operator`"] - virt-operator("`container: - virt-operator`") - virt-operator-proxy{{"container: - proxy"}} - virt-operator --> virt-operator-proxy - virt-operator-proxy --> virt-operator - end - - subgraph p-virt-controller-deploy ["`Deploy/virt-controller`"] - virt-controller("`container: - virt-controller`") - virt-controller-proxy{{"container: - proxy"}} - virt-controller --> virt-controller-proxy - virt-controller-proxy --> virt-controller - end - subgraph p-virt-api-deploy ["`Deploy/virt-api`"] - virt-api("`container: - virt-api`") - virt-api-proxy{{"container: - proxy"}} - virt-api --> virt-api-proxy - virt-api-proxy --> virt-api - end - - subgraph p-virt-handler-deploy ["`DaemonSet/virt-handler`"] - virt-handler("`container: - virt-handler`") - virt-handler-proxy{{"container: - proxy"}} - virt-handler --> virt-handler-proxy - virt-handler-proxy --> virt-handler - end - end - - subgraph kubeapi ["control-plane"] - kube-api["`Kubernetes API Server`"] - end - - virt-operator-proxy <----> kube-api - virt-controller-proxy <----> kube-api - virt-api-proxy <----> kube-api - virt-handler-proxy <----> kube-api - - subgraph cdi ["CDI"] - subgraph cdi-operator-deploy ["`Deploy/cdi-operator`"] - cdi-operator-proxy{{"container: - proxy"}} - cdi-operator("`container: - virt-handler`") - cdi-operator --> cdi-operator-proxy - cdi-operator-proxy --> cdi-operator - end - - subgraph cdi-deployment-deploy ["`Deploy/cdi-deployment`"] - cdi-deployment-proxy{{"container: - proxy"}} - cdi-deployment("`container: - cdi-eployment`") - cdi-deployment --> cdi-deployment-proxy - cdi-deployment-proxy --> cdi-deployment - end - - subgraph cdi-api-deploy ["`Deploy/cdi-api`"] - cdi-api-proxy{{"container: - proxy"}} - cdi-api("`container: - cdi-api`") - cdi-api --> cdi-api-proxy - cdi-api-proxy --> cdi-api - end - - subgraph cdi-exportproxy-deploy ["`Deploy/cdi-exportproxy`"] - cdi-exportproxy-proxy{{"container: - proxy"}} - cdi-exportproxy("`container: - cdi-exportproxy`") - cdi-exportproxy --> cdi-exportproxy-proxy - cdi-exportproxy-proxy --> cdi-exportproxy - end - end - kube-api <----> cdi-operator-proxy - kube-api <----> cdi-deployment-proxy - kube-api <----> cdi-api-proxy - kube-api <----> cdi-exportproxy-proxy - - - subgraph d8virt ["D8 API"] - subgraph d8-virt-deploy ["Deploy/virtualization-controller"] - d8-virt-controller-proxy("`container: - proxy`") - d8-virt-controller("`container: - virtualization-controller`") - d8-virt-controller --> d8-virt-controller-proxy - d8-virt-controller-proxy --> d8-virt-controller - end - end - - kube-api <----> d8-virt-controller-proxy -``` - -Variation (block diagram seems not so powerful as flowchart) -```mermaid -block-beta - columns 5 - - %% Main containers in kubevirt Pods - virtoperator["virt-operator"] - virtapi["virt-api"] - virtcontroller["virt-controller"] - virthandler["virt-handler"] - virtexportproxy["virt-exportproxy"] - - %% Space for links. - space:5 - %% Links between containers. - virtoperator --> virtoperatorproxy - %%virtoperatorproxy --> virtoperator - virtapi --> virtapiproxy - virtcontroller --> virtcontrollerproxy - virthandler --> virthandlerproxy - virtexportproxy --> virtexportproxyproxy - - %% Proxies in kubevirt Pods. - virtoperatorproxy(["proxy"]) - virtapiproxy(["proxy"]) - virtcontrollerproxy(["proxy"]) - virthandlerproxy(["proxy"]) - virtexportproxyproxy(["proxy"]) - - space:5 - - space - kubeapiserver{{"Kubernetes API Server"}}:3 - space - - virtoperatorproxy --> kubeapiserver - %%kubeapiserver --> virtoperatorproxy - virtapiproxy --> kubeapiserver - virtcontrollerproxy --> kubeapiserver - virthandlerproxy --> kubeapiserver - virtexportproxyproxy --> kubeapiserver - - space:5 - cdioperatorproxy --> kubeapiserver - cdiapiproxy --> kubeapiserver - cdideploymentproxy --> kubeapiserver - cdiuploadproxyproxy --> kubeapiserver - virtualizationcontrollerproxy --> kubeapiserver - - %% Proxies in CDI Pods. - cdioperatorproxy(["proxy"]) - cdiapiproxy(["proxy"]) - cdideploymentproxy(["proxy"]) - cdiuploadproxyproxy(["proxy"]) - virtualizationcontrollerproxy(["proxy"]) - - %% Links inside CDI Pods. - space:5 - cdioperator --> cdioperatorproxy - cdiapi--> cdiapiproxy - cdideployment --> cdideploymentproxy - cdiuploadproxy --> cdiuploadproxyproxy - virtualizationcontroller --> virtualizationcontrollerproxy - - cdioperator["cdi-operator"] - cdiapi["cdi-api"] - cdideployment["cdi-deployment"] - cdiuploadproxy["cdi-uploadproxy"] - virtualizationcontroller["virtualization- - controller"] -``` - -### Changes to add proxy to the Pod -- Add a ConfigMap with a simple kubeconfig points to the local proxy. - ``` - ... - clusters: - - cluster: - server: http://127.0.0.1:23915 - ... - ``` -- Add a volume and a volumeMount to pass new kubeconfig as file to the main container. -- Set KUBECONFIG variable in the main container. File should contain configuration to connect to proxy port. - - Note: kubevirt containers use --kubeconfig flag, cdi containers use KUBECONFIG env variable. -- Add a new sidecar container with the proxy. - - Set WEBHOOK_ADDRESS if webhook proxying is required. - - Add volumeMount with a certificate and set WEBHOOK_CERT_FILE and WEBHOOK_KEY_FILE to use the certificate. - - Add port 24192 to the webhook Service to use the certificate without issuing new one with changed ServerName. - -## API client proxying - -Implemented rewrites: -- apiGroup, kind, metadata.ownerReferences for Kubevirt and CDI Custom Resources. -- metadata.ownerReferences for Pod -- rules for Role, ClusterRole -- webhooks[].rules for ValidatingWebhookConfiguration, MutatingWebhookConfiguration -- metadata.name, spec.group, spec.names for CustomResourceDefinition. -- patch /spec for CustomResourceDefinition. -- fieldSelector=metadata.name=&watch=true for CRD. -- request.resource, request.object, request.kind, etc. for AdmissionReview. - -TODO: -- labels and annotations for Kubevirt and CDI CRs and all kubevirt related resources, Nodes and Pods. -- patches in general. -- SubjectAccessReview https://dev-k8sref-io.web.app/docs/authorization/subjectaccessreview-v1/ - -```plantuml -@startuml -box "Pod with Controller" #fff -participant "container\nname: controller" as ctrl -note over ctrl -Use KUBECONFIG file to connect -to local proxy instead of -directly using API server: -""clusters:"" -""- cluster:"" -"" server: http://127.0.0.1:23915"" -endnote -queue "additional container\nname: proxy" as proxy -/ note over proxy -Listen on ""127.0.0.1:23915"" -and pass requests to -Kubernetes API Server -endnote -endbox -box "Control Plane" #fff -participant "Kubernetes\nAPI Server" as kube_api -endbox - -== Get, List, Delete operations == - -ctrl -> proxy : Request operation via endpoint:\n\n/apis/kubevirt.io/v1/virtualmachines -proxy -> kube_api : Rewrite endpoint, pass request to:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines - -kube_api -> proxy : Response with renamed resources:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine -proxy -> ctrl : Rewrite payload, pass\nresponse with restored resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine - -== Create, Update, Patch operations == - -ctrl -> proxy : Request operation via endpoint:\n\n/apis/kubevirt.io/v1/virtualmachines\n\nA payload contains original resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine -proxy -> kube_api : Rewrite endpoint and payload,\npass request with renamed resources:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine - -kube_api -> proxy : Response with renamed resources:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine -proxy -> ctrl : Rewrite payload, pass\nresponse with restored resources:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine - -== Watch operation == - -ctrl -> proxy : Request WATCH operation via endpoint:\n\n/apis/kubevirt.io↩︎\n/v1/virtualmachines?watch=true -activate proxy -proxy -> kube_api : Rewrite endpoint, pass request to:\n\n/apis/x.virtualization.deckhouse.io↩︎\n/v1/prefixedvirtualmachines?watch=true -activate kube_api - -kube_api -> kube_api : Generate\nWATCH\nevents - -kube_api -> proxy : ADDED, MODIFIED or DELETED\nevent with renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine -activate proxy -proxy -> ctrl : Rewrite payload, pass\nevent with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine -deactivate proxy - -kube_api -> proxy : BOOKMARK event with renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine -activate proxy -proxy -> ctrl : Rewrite payload, pass\nevent with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine -deactivate proxy - -kube_api -> proxy : Stop WATCH operation -deactivate kube_api -proxy -> ctrl : Stop WATCH operation -deactivate proxy - -@endplantuml -``` - - -## Webhook proxying - -Kubernetes API Server connects to proxy, so proxy will pass AdmissionReview to real webhook. Proxy may rewrite JSON payloads -for different purposes, e.g. resources renaming. - -Additional changes: - -- A targetPort in the webhook Service should point to proxy container. -- A proxy container should mount secret with certificates. - -```plantuml -@startuml -box "Pod with Controller" #fff -participant "container\nname: controller" as ctrl -queue "additional container\nname: proxy" as proxy -endbox -box "Control Plane" #fff -participant "Kubernetes\nAPI Server" as kube_api -endbox - -note over ctrl -Listen on ""0.0.0.0:9443"" -endnote -/ note over proxy -Listen on ""0.0.0.0:24192"" -and pass requests to -the controller ""127.0.0.1:9443"" -endnote -/ note over kube_api -Pass AdmissionReview to Pod -endnote - -== Webhook handling == - -kube_api -> proxy : Request admission review via\nconfigured endpoint:\n\n/validate-x-virtualization-↩︎\ndeckhouse-io-prefixed-virtualmachines\n\nA payload contains renamed resource:\n\napiVersion: x.virtualization.deckhouse.io/v1\nkind: PrefixedVirtualMachine -proxy -> ctrl : Rewrite admission review, pass\nrequest with restored resource:\n\napiVersion: kubevirt.io/v1\nkind: VirtualMachine - -... Validating webhook response ... -ctrl -> proxy : AdmissionReview response -proxy -> kube_api : No rewrite, pass as-is. - -... Mutating webhook response ... -ctrl -> proxy : AdmissionReview response\nwith the patch -proxy -> kube_api : Rewrite ownerRef patch if\nresponse.patchType == JSONPatch\nand patch operates on the ownerRef content - - -@enduml -``` - -```mermaid ---- -config: - htmlLabels: false ---- - -sequenceDiagram - - box Pod with controller - participant ctrl as container
name: controller - participant proxy as container
name: proxy - end - - Note over ctrl: Listen on 0.0.0.0:9443 - Note over proxy: Listen on 0.0.0.0:24192
and pass requests to
127.0.0.1:9443 - - box Control plane - participant kubeapi as Kubernetes
API Server - end - note over kubeapi: Request webhook with AdmissionReview - - kubeapi --> ctrl: Webhook handling - - kubeapi ->>+ proxy: Send AdmissionReview with
renamed resources
apiVersion: x.virtualization.deckhouse.io
PrefixedVirtualMachine - - proxy ->>+ ctrl: Proxy restores resource:
apiGroup, kind, ownerReferences
apiVersion: kubevirt.io
kind: VirtualMachine - - ctrl ->>- proxy: AdmissionReview
with webhook response - - alt Validating webhook response - proxy ->> kubeapi: No rewrite, pass as-is - else Mutating webhook response - proxy ->>- kubeapi: Rewrite patch if
ownerReferences is modified - end - - - - %%participant Bob - %% ctrl->>John: "`This **is** _Markdown_`" - %%loop HealthCheck - %% John->>John: Fight against hypochondria - %%end - %%Note right of John: Rational thoughts
prevail! - %%John-->>ctrl: Great! - %%John->>Bob: How about you? - %%Bob-->>John: Jolly good! -``` +_WIP_ diff --git a/images/kube-api-rewriter/Taskfile.dist.yaml b/images/kube-api-rewriter/Taskfile.dist.yaml index cc0f0de..8ba8a68 100644 --- a/images/kube-api-rewriter/Taskfile.dist.yaml +++ b/images/kube-api-rewriter/Taskfile.dist.yaml @@ -28,13 +28,6 @@ tasks: vars: CTR_COMMAND: "['./kube-api-rewriter']" - dev:deploy-with-dlv: - desc: "apply manifest with kube-api-rewriter with dlv and test-controller" - cmds: - - task: dev:__deploy - vars: - CTR_COMMAND: "['./dlv', '--listen=:2345', '--headless=true', '--continue', '--log=true', '--log-output=debugger,debuglineerr,gdbwire,lldbout,rpc', '--accept-multiclient', '--api-version=2', 'exec', './kube-api-rewriter']" - dev:__deploy: internal: true cmds: diff --git a/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go b/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go index 23d3d13..dc80460 100644 --- a/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go +++ b/images/kube-api-rewriter/cmd/kube-api-rewriter/main.go @@ -21,11 +21,11 @@ import ( "net/http" "os" - "github.com/deckhouse/kube-api-rewriter/pkg/kubevirt" logutil "github.com/deckhouse/kube-api-rewriter/pkg/log" "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/healthz" "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/metrics" "github.com/deckhouse/kube-api-rewriter/pkg/monitoring/profiler" + "github.com/deckhouse/kube-api-rewriter/pkg/operatornelm" "github.com/deckhouse/kube-api-rewriter/pkg/proxy" "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" "github.com/deckhouse/kube-api-rewriter/pkg/server" @@ -80,7 +80,7 @@ func main() { }) // Load rules from file or use default kubevirt rules. - rewriteRules := kubevirt.KubevirtRewriteRules + rewriteRules := operatornelm.OperatorNelmRewriteRules if os.Getenv("RULES_PATH") != "" { rulesFromFile, err := rewriter.LoadRules(os.Getenv("RULES_PATH")) if err != nil { diff --git a/images/kube-api-rewriter/go.mod b/images/kube-api-rewriter/go.mod index 6385af8..1a6e730 100644 --- a/images/kube-api-rewriter/go.mod +++ b/images/kube-api-rewriter/go.mod @@ -1,19 +1,19 @@ module github.com/deckhouse/kube-api-rewriter -go 1.24.13 +go 1.25.0 require ( github.com/fsnotify/fsnotify v1.9.0 github.com/josephburnett/jd v1.9.2 github.com/kr/text v0.2.0 - github.com/prometheus/client_golang v1.23.0 - github.com/stretchr/testify v1.10.0 + github.com/prometheus/client_golang v1.23.2 + github.com/stretchr/testify v1.11.1 github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 - k8s.io/api v0.33.3 - k8s.io/apimachinery v0.33.3 - k8s.io/client-go v0.33.3 - sigs.k8s.io/controller-runtime v0.21.0 + k8s.io/api v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/client-go v0.35.0 + sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/yaml v1.6.0 ) @@ -28,43 +28,40 @@ require ( github.com/go-openapi/jsonpointer v0.21.1 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.1 // indirect - 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/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.65.0 // indirect + github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.17.0 // indirect - github.com/spf13/pflag v1.0.7 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/x448/float16 v0.8.4 // indirect - go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.42.0 // indirect + golang.org/x/net v0.47.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.34.0 // indirect - golang.org/x/term v0.33.0 // indirect - golang.org/x/text v0.27.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect golang.org/x/time v0.12.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // 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/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect ) replace google.golang.org/protobuf => google.golang.org/protobuf v1.33.0 diff --git a/images/kube-api-rewriter/go.sum b/images/kube-api-rewriter/go.sum index 6961820..6dd7074 100644 --- a/images/kube-api-rewriter/go.sum +++ b/images/kube-api-rewriter/go.sum @@ -1,7 +1,7 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= 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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -28,17 +28,13 @@ github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZ github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.6.0/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/gofuzz v1.0.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/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= 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/josephburnett/jd v1.9.2 h1:ECJRRFXCCqbtidkAHckHGSZm/JIaAxS1gygHLF8MI5Y= @@ -47,8 +43,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm 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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -62,36 +56,35 @@ github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUt 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/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.0 h1:ust4zpdl9r4trLY/gSjlm07PuiBq2ynaXXlptpfy8Uc= -github.com/prometheus/client_golang v1.23.0/go.mod h1:i/o0R9ByOnHX0McrTMTyhYvKE4haaf2mW08I+jGAjEE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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.65.0 h1:QDwzd+G1twt//Kwj/Ww6E9FQq1iVMmODnILtW1t2VzE= -github.com/prometheus/common v0.65.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= -github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= -github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -104,113 +97,68 @@ 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= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 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= -go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= -go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= -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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= -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-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= -golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ= -golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= -golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= -golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 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= -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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= -golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -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= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.3 h1:SRd5t//hhkI1buzxb288fy2xvjubstenEKL9K51KBI8= -k8s.io/api v0.33.3/go.mod h1:01Y/iLUjNBM3TAvypct7DIj0M0NIZc+PzAHCIo0CYGE= -k8s.io/apiextensions-apiserver v0.33.0 h1:d2qpYL7Mngbsc1taA4IjJPRJ9ilnsXIrndH+r9IimOs= -k8s.io/apiextensions-apiserver v0.33.0/go.mod h1:VeJ8u9dEEN+tbETo+lFkwaaZPg6uFKLGj5vyNEwwSzc= -k8s.io/apimachinery v0.33.3 h1:4ZSrmNa0c/ZpZJhAgRdcsFcZOw1PQU1bALVQ0B3I5LA= -k8s.io/apimachinery v0.33.3/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.3 h1:M5AfDnKfYmVJif92ngN532gFqakcGi6RvaOF16efrpA= -k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= 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-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= -k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= -k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= -sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= -sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/kube-api-rewriter/mount-points.yaml b/images/kube-api-rewriter/mount-points.yaml index fa5ef6d..eefff43 100644 --- a/images/kube-api-rewriter/mount-points.yaml +++ b/images/kube-api-rewriter/mount-points.yaml @@ -1,7 +1 @@ -# A list of pre-created mount points for containerd strict mode. - -dirs: - - /etc/virt-operator/certificates - - /etc/virt-api/certificates - # Create dirs in /run, as /var/run is a symlink to /run. - - /run/certs/cdi-apiserver-server-cert +dirs: [] diff --git a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go b/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go deleted file mode 100644 index cc89f3c..0000000 --- a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules.go +++ /dev/null @@ -1,698 +0,0 @@ -/* -Copyright 2024 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 kubevirt - -import ( - . "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" -) - -const ( - internalPrefix = "internal.virtualization.deckhouse.io" - nodePrefix = "node.virtualization.deckhouse.io" - rootPrefix = "virtualization.deckhouse.io" -) - -var KubevirtRewriteRules = &RewriteRules{ - KindPrefix: "InternalVirtualization", // VirtualMachine -> InternalVirtualizationVirtualMachine - ResourceTypePrefix: "internalvirtualization", // virtualmachines -> internalvirtualizationvirtualmachines - ShortNamePrefix: "intvirt", // kubectl get intvirtvm - Categories: []string{"intvirt"}, // kubectl get intvirt to see all KubeVirt and CDI resources. - Rules: KubevirtAPIGroupsRules, - Webhooks: KubevirtWebhooks, - Labels: MetadataReplace{ - Names: []MetadataReplaceRule{ - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, - {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, - {Original: "operator.kubevirt.io", Renamed: "operator.kubevirt." + internalPrefix}, - {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, - {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, - // Special cases. - {Original: "node-labeller.kubevirt.io/skip-node", Renamed: "node-labeller." + rootPrefix + "/skip-node"}, - {Original: "node-labeller.kubevirt.io/obsolete-host-model", Renamed: "node-labeller." + internalPrefix + "/obsolete-host-model"}, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-operator", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-operator-internal-virtualization", - }, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "cdi-controller", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "cdi-controller-internal-virtualization", - }, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "virt-operator", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "virt-operator-internal-virtualization", - }, - { - Original: "app.kubernetes.io/managed-by", OriginalValue: "kubevirt-operator", - Renamed: "app.kubernetes.io/managed-by", RenamedValue: "kubevirt-operator-internal-virtualization", - }, - }, - Prefixes: []MetadataReplaceRule{ - // CDI related labels. - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, - {Original: "prometheus.cdi.kubevirt.io", Renamed: "prometheus.cdi." + internalPrefix}, - {Original: "upload.cdi.kubevirt.io", Renamed: "upload.cdi." + internalPrefix}, - // KubeVirt related labels. - {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, - {Original: "prometheus.kubevirt.io", Renamed: "prometheus.kubevirt." + internalPrefix}, - {Original: "operator.kubevirt.io", Renamed: "operator.kubevirt." + internalPrefix}, - {Original: "vm.kubevirt.io", Renamed: "vm.kubevirt." + internalPrefix}, - // Node features related labels. - // Note: these labels are not "internal". - {Original: "cpu-feature.node.kubevirt.io", Renamed: "cpu-feature." + nodePrefix}, - {Original: "cpu-model-migration.node.kubevirt.io", Renamed: "cpu-model-migration." + nodePrefix}, - {Original: "cpu-model.node.kubevirt.io", Renamed: "cpu-model." + nodePrefix}, - {Original: "cpu-timer.node.kubevirt.io", Renamed: "cpu-timer." + nodePrefix}, - {Original: "cpu-vendor.node.kubevirt.io", Renamed: "cpu-vendor." + nodePrefix}, - {Original: "scheduling.node.kubevirt.io", Renamed: "scheduling." + nodePrefix}, - {Original: "host-model-cpu.node.kubevirt.io", Renamed: "host-model-cpu." + nodePrefix}, - {Original: "host-model-required-features.node.kubevirt.io", Renamed: "host-model-required-features." + nodePrefix}, - {Original: "hyperv.node.kubevirt.io", Renamed: "hyperv." + nodePrefix}, - {Original: "machine-type.node.kubevirt.io", Renamed: "machine-type." + nodePrefix}, - }, - }, - Annotations: MetadataReplace{ - Prefixes: []MetadataReplaceRule{ - // CDI related annotations. - {Original: "cdi.kubevirt.io", Renamed: "cdi." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, - // KubeVirt related annotations. - {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, - {Original: "certificates.kubevirt.io", Renamed: "certificates.kubevirt." + internalPrefix}, - }, - }, - Finalizers: MetadataReplace{ - Prefixes: []MetadataReplaceRule{ - {Original: "kubevirt.io", Renamed: "kubevirt." + internalPrefix}, - {Original: "operator.cdi.kubevirt.io", Renamed: "operator.cdi." + internalPrefix}, - }, - }, - Excludes: []ExcludeRule{ - ExcludeRule{ - Kinds: []string{ - "PersistentVolumeClaim", - "PersistentVolume", - "Pod", - }, - MatchLabels: map[string]string{ - "app.kubernetes.io/managed-by": "cdi-controller", - }, - }, - ExcludeRule{ - Kinds: []string{ - "CDI", - }, - MatchNames: []string{ - "cdi", - }, - }, - }, -} - -// TODO create generator in golang to produce below rules from Kubevirt and CDI sources so proxy can work with future versions. - -var KubevirtAPIGroupsRules = map[string]APIGroupRule{ - "cdi.kubevirt.io": { - GroupRule: GroupRule{ - Group: "cdi.kubevirt.io", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Renamed: "cdi." + internalPrefix, - }, - ResourceRules: map[string]ResourceRule{ - // cdiconfigs.cdi.kubevirt.io - "cdiconfigs": { - Kind: "CDIConfig", - ListKind: "CDIConfigList", - Plural: "cdiconfigs", - Singular: "cdiconfig", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // cdis.cdi.kubevirt.io - "cdis": { - Kind: "CDI", - ListKind: "CDIList", - Plural: "cdis", - Singular: "cdi", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{"cdi", "cdis"}, - }, - // dataimportcrons.cdi.kubevirt.io - "dataimportcrons": { - Kind: "DataImportCron", - ListKind: "DataImportCronList", - Plural: "dataimportcrons", - Singular: "dataimportcron", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"dic", "dics"}, - }, - // datasources.cdi.kubevirt.io - "datasources": { - Kind: "DataSource", - ListKind: "DataSourceList", - Plural: "datasources", - Singular: "datasource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"das"}, - }, - // datavolumes.cdi.kubevirt.io - "datavolumes": { - Kind: "DataVolume", - ListKind: "DataVolumeList", - Plural: "datavolumes", - Singular: "datavolume", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{"all"}, - ShortNames: []string{"dv", "dvs"}, - }, - // objecttransfers.cdi.kubevirt.io - "objecttransfers": { - Kind: "ObjectTransfer", - ListKind: "ObjectTransferList", - Plural: "objecttransfers", - Singular: "objecttransfer", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{"ot", "ots"}, - }, - // storageprofiles.cdi.kubevirt.io - "storageprofiles": { - Kind: "StorageProfile", - ListKind: "StorageProfileList", - Plural: "storageprofiles", - Singular: "storageprofile", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // volumeclonesources.cdi.kubevirt.io - "volumeclonesources": { - Kind: "VolumeCloneSource", - ListKind: "VolumeCloneSourceList", - Plural: "volumeclonesources", - Singular: "volumeclonesource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // volumeimportsources.cdi.kubevirt.io - "volumeimportsources": { - Kind: "VolumeImportSource", - ListKind: "VolumeImportSourceList", - Plural: "volumeimportsources", - Singular: "volumeimportsource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - // volumeuploadsources.cdi.kubevirt.io - "volumeuploadsources": { - Kind: "VolumeUploadSource", - ListKind: "VolumeUploadSourceList", - Plural: "volumeuploadsources", - Singular: "volumeuploadsource", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Categories: []string{}, - ShortNames: []string{}, - }, - }, - }, - "forklift.cdi.kubevirt.io": { - GroupRule: GroupRule{ - Group: "forklift.cdi.kubevirt.io", - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - Renamed: "forklift.cdi." + internalPrefix, - }, - ResourceRules: map[string]ResourceRule{ - // openstackvolumepopulators.forklift.cdi.kubevirt.io - "openstackvolumepopulators": { - Kind: "OpenstackVolumePopulator", - ListKind: "OpenstackVolumePopulatorList", - Plural: "openstackvolumepopulators", - Singular: "openstackvolumepopulator", - ShortNames: []string{"osvp", "osvps"}, - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - }, - // ovirtvolumepopulators.forklift.cdi.kubevirt.io - "ovirtvolumepopulators": { - Kind: "OvirtVolumePopulator", - ListKind: "OvirtVolumePopulatorList", - Plural: "ovirtvolumepopulators", - Singular: "ovirtvolumepopulator", - ShortNames: []string{"ovvp", "ovvps"}, - Versions: []string{"v1beta1"}, - PreferredVersion: "v1beta1", - }, - }, - }, - "kubevirt.io": { - GroupRule: GroupRule{ - Group: "kubevirt.io", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Renamed: "internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // kubevirts.kubevirt.io - "kubevirts": { - Kind: "KubeVirt", - ListKind: "KubeVirtList", - Plural: "kubevirts", - Singular: "kubevirt", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"kv", "kvs"}, - }, - // virtualmachines.kubevirt.io - "virtualmachines": { - Kind: "VirtualMachine", - ListKind: "VirtualMachineList", - Plural: "virtualmachines", - Singular: "virtualmachine", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"vm", "vms"}, - }, - // virtualmachineinstances.kubevirt.io - "virtualmachineinstances": { - Kind: "VirtualMachineInstance", - ListKind: "VirtualMachineInstanceList", - Plural: "virtualmachineinstances", - Singular: "virtualmachineinstance", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"vmi", "vmsi"}, - }, - // virtualmachineinstancemigrations.kubevirt.io - "virtualmachineinstancemigrations": { - Kind: "VirtualMachineInstanceMigration", - ListKind: "VirtualMachineInstanceMigrationList", - Plural: "virtualmachineinstancemigrations", - Singular: "virtualmachineinstancemigration", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"vmim", "vmims"}, - }, - // virtualmachineinstancepresets.kubevirt.io - "virtualmachineinstancepresets": { - Kind: "VirtualMachineInstancePreset", - ListKind: "VirtualMachineInstancePresetList", - Plural: "virtualmachineinstancepresets", - Singular: "virtualmachineinstancepreset", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"vmipreset", "vmipresets"}, - }, - // virtualmachineinstancereplicasets.kubevirt.io - "virtualmachineinstancereplicasets": { - Kind: "VirtualMachineInstanceReplicaSet", - ListKind: "VirtualMachineInstanceReplicaSetList", - Plural: "virtualmachineinstancereplicasets", - Singular: "virtualmachineinstancereplicaset", - Versions: []string{"v1", "v1alpha3"}, - PreferredVersion: "v1", - Categories: []string{"all"}, - ShortNames: []string{"vmirs", "vmirss"}, - }, - }, - }, - "clone.kubevirt.io": { - GroupRule: GroupRule{ - Group: "clone.kubevirt.io", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Renamed: "clone.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // virtualmachineclones.clone.kubevirt.io - "virtualmachineclones": { - Kind: "VirtualMachineClone", - ListKind: "VirtualMachineCloneList", - Plural: "virtualmachineclones", - Singular: "virtualmachineclone", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmclone", "vmclones"}, - }, - }, - }, - "export.kubevirt.io": { - GroupRule: GroupRule{ - Group: "export.kubevirt.io", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Renamed: "export.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // virtualmachineexports.export.kubevirt.io - "virtualmachineexports": { - Kind: "VirtualMachineExport", - ListKind: "VirtualMachineExportList", - Plural: "virtualmachineexports", - Singular: "virtualmachineexport", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmexport", "vmexports"}, - }, - }, - }, - "instancetype.kubevirt.io": { - GroupRule: GroupRule{ - Group: "instancetype.kubevirt.io", - Versions: []string{"v1alpha1", "v1alpha2"}, - PreferredVersion: "v1alpha2", - Renamed: "instancetype.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // virtualmachineinstancetypes.instancetype.kubevirt.io - "virtualmachineinstancetypes": { - Kind: "VirtualMachineInstancetype", - ListKind: "VirtualMachineInstancetypeList", - Plural: "virtualmachineinstancetypes", - Singular: "virtualmachineinstancetype", - Versions: []string{"v1alpha1", "v1alpha2"}, - PreferredVersion: "v1alpha2", - Categories: []string{"all"}, - ShortNames: []string{"vminstancetype", "vminstancetypes", "vmf", "vmfs"}, - }, - // virtualmachinepreferences.instancetype.kubevirt.io - "virtualmachinepreferences": { - Kind: "VirtualMachinePreference", - ListKind: "VirtualMachinePreferenceList", - Plural: "virtualmachinepreferences", - Singular: "virtualmachinepreference", - Versions: []string{"v1alpha1", "v1alpha2"}, - PreferredVersion: "v1alpha2", - Categories: []string{"all"}, - ShortNames: []string{"vmpref", "vmprefs", "vmp", "vmps"}, - }, - // virtualmachineclusterinstancetypes.instancetype.kubevirt.io - "virtualmachineclusterinstancetypes": { - Kind: "VirtualMachineClusterInstancetype", - ListKind: "VirtualMachineClusterInstancetypeList", - Plural: "virtualmachineclusterinstancetypes", - Singular: "virtualmachineclusterinstancetype", - Versions: []string{"v1alpha1", "v1alpha2"}, - PreferredVersion: "v1alpha2", - Categories: []string{}, - ShortNames: []string{"vmclusterinstancetype", "vmclusterinstancetypes", "vmcf", "vmcfs"}, - }, - // virtualmachineclusterpreferences.instancetype.kubevirt.io - "virtualmachineclusterpreferences": { - Kind: "VirtualMachineClusterPreference", - ListKind: "VirtualMachineClusterPreferenceList", - Plural: "virtualmachineclusterpreferences", - Singular: "virtualmachineclusterpreference", - Versions: []string{"v1alpha1", "v1alpha2"}, - PreferredVersion: "v1alpha2", - Categories: []string{}, - ShortNames: []string{"vmcp", "vmcps"}, - }, - }, - }, - "migrations.kubevirt.io": { - GroupRule: GroupRule{ - Group: "migrations.kubevirt.io", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Renamed: "migrations.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // migrationpolicies.migrations.kubevirt.io - "migrationpolicies": { - Kind: "MigrationPolicy", - ListKind: "MigrationPolicyList", - Plural: "migrationpolicies", - Singular: "migrationpolicy", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{}, - }, - }, - }, - "pool.kubevirt.io": { - GroupRule: GroupRule{ - Group: "pool.kubevirt.io", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Renamed: "pool.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // virtualmachinepools.pool.kubevirt.io - "virtualmachinepools": { - Kind: "VirtualMachinePool", - ListKind: "VirtualMachinePoolList", - Plural: "virtualmachinepools", - Singular: "virtualmachinepool", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmpool", "vmpools"}, - }, - }, - }, - "snapshot.kubevirt.io": { - GroupRule: GroupRule{ - Group: "snapshot.kubevirt.io", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Renamed: "snapshot.internal.virtualization.deckhouse.io", - }, - ResourceRules: map[string]ResourceRule{ - // virtualmachinerestores.snapshot.kubevirt.io - "virtualmachinerestores": { - Kind: "VirtualMachineRestore", - ListKind: "VirtualMachineRestoreList", - Plural: "virtualmachinerestores", - Singular: "virtualmachinerestore", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmrestore", "vmrestores"}, - }, - // virtualmachinesnapshotcontents.snapshot.kubevirt.io - "virtualmachinesnapshotcontents": { - Kind: "VirtualMachineSnapshotContent", - ListKind: "VirtualMachineSnapshotContentList", - Plural: "virtualmachinesnapshotcontents", - Singular: "virtualmachinesnapshotcontent", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmsnapshotcontent", "vmsnapshotcontents"}, - }, - // virtualmachinesnapshots.snapshot.kubevirt.io - "virtualmachinesnapshots": { - Kind: "VirtualMachineSnapshot", - ListKind: "VirtualMachineSnapshotList", - Plural: "virtualmachinesnapshots", - Singular: "virtualmachinesnapshot", - Versions: []string{"v1alpha1"}, - PreferredVersion: "v1alpha1", - Categories: []string{"all"}, - ShortNames: []string{"vmsnapshot", "vmsnapshots"}, - }, - }, - }, -} - -var KubevirtWebhooks = map[string]WebhookRule{ - // CDI webhooks. - // Run this in original CDI installation: - // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l cdi.kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' -r - // TODO create generator in golang to extract these rules from resource definitions in the cdi-operator package. - "/datavolume-mutate": { - Path: "/datavolume-mutate", - Group: "cdi.kubevirt.io", - Resource: "datavolumes", - }, - "/dataimportcron-validate": { - Path: "/dataimportcron-validate", - Group: "cdi.kubevirt.io", - Resource: "dataimportcrons", - }, - "/datavolume-validate": { - Path: "/datavolume-validate", - Group: "cdi.kubevirt.io", - Resource: "datavolumes", - }, - "/cdi-validate": { - Path: "/cdi-validate", - Group: "cdi.kubevirt.io", - Resource: "cdis", - }, - "/objecttransfer-validate": { - Path: "/objecttransfer-validate", - Group: "cdi.kubevirt.io", - Resource: "objecttransfers", - }, - "/populator-validate": { - Path: "/populator-validate", - Group: "cdi.kubevirt.io", - Resource: "volumeimportsources", // Also, volumeuploadsources. This field for logging only. - }, - - // Kubevirt webhooks. - // Run this in original Kubevirt installation: - // kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration -l kubevirt.io -o json | jq '.items[] | .webhooks[] | {"path": .clientConfig.service.path, "group": (.rules[]|.apiGroups|join(",")), "resource": (.rules[]|.resources|join(",")) } | "\""+.path +"\": {\nPath: \"" + .path + "\",\nGroup: \"" + .group + "\",\nResource: \"" + .resource + "\",\n}," ' - // TODO create generator in golang to extract these rules from resource definitions in the virt-operator package. - "/virtualmachineinstances-validate-create": { - Path: "/virtualmachineinstances-validate-create", - Group: "kubevirt.io", - Resource: "virtualmachineinstances", - }, - "/virtualmachineinstances-validate-update": { - Path: "/virtualmachineinstances-validate-update", - Group: "kubevirt.io", - Resource: "virtualmachineinstances", - }, - "/virtualmachines-validate": { - Path: "/virtualmachines-validate", - Group: "kubevirt.io", - Resource: "virtualmachines", - }, - "/virtualmachinereplicaset-validate": { - Path: "/virtualmachinereplicaset-validate", - Group: "kubevirt.io", - Resource: "virtualmachineinstancereplicasets", - }, - "/virtualmachinepool-validate": { - Path: "/virtualmachinepool-validate", - Group: "pool.kubevirt.io", - Resource: "virtualmachinepools", - }, - "/vmipreset-validate": { - Path: "/vmipreset-validate", - Group: "kubevirt.io", - Resource: "virtualmachineinstancepresets", - }, - "/migration-validate-create": { - Path: "/migration-validate-create", - Group: "kubevirt.io", - Resource: "virtualmachineinstancemigrations", - }, - "/migration-validate-update": { - Path: "/migration-validate-update", - Group: "kubevirt.io", - Resource: "virtualmachineinstancemigrations", - }, - "/virtualmachinesnapshots-validate": { - Path: "/virtualmachinesnapshots-validate", - Group: "snapshot.kubevirt.io", - Resource: "virtualmachinesnapshots", - }, - "/virtualmachinerestores-validate": { - Path: "/virtualmachinerestores-validate", - Group: "snapshot.kubevirt.io", - Resource: "virtualmachinerestores", - }, - "/virtualmachineexports-validate": { - Path: "/virtualmachineexports-validate", - Group: "export.kubevirt.io", - Resource: "virtualmachineexports", - }, - "/virtualmachineinstancetypes-validate": { - Path: "/virtualmachineinstancetypes-validate", - Group: "instancetype.kubevirt.io", - Resource: "virtualmachineinstancetypes", - }, - "/virtualmachineclusterinstancetypes-validate": { - Path: "/virtualmachineclusterinstancetypes-validate", - Group: "instancetype.kubevirt.io", - Resource: "virtualmachineclusterinstancetypes", - }, - "/virtualmachinepreferences-validate": { - Path: "/virtualmachinepreferences-validate", - Group: "instancetype.kubevirt.io", - Resource: "virtualmachinepreferences", - }, - "/virtualmachineclusterpreferences-validate": { - Path: "/virtualmachineclusterpreferences-validate", - Group: "instancetype.kubevirt.io", - Resource: "virtualmachineclusterpreferences", - }, - "/status-validate": { - Path: "/status-validate", - Group: "kubevirt.io", - Resource: "virtualmachines/status,virtualmachineinstancereplicasets/status,virtualmachineinstancemigrations/status", - }, - "/migration-policy-validate-create": { - Path: "/migration-policy-validate-create", - Group: "migrations.kubevirt.io", - Resource: "migrationpolicies", - }, - "/vm-clone-validate-create": { - Path: "/vm-clone-validate-create", - Group: "clone.kubevirt.io", - Resource: "virtualmachineclones", - }, - "/kubevirt-validate-delete": { - Path: "/kubevirt-validate-delete", - Group: "kubevirt.io", - Resource: "kubevirts", - }, - "/kubevirt-validate-update": { - Path: "/kubevirt-validate-update", - Group: "kubevirt.io", - Resource: "kubevirts", - }, - "/virtualmachines-mutate": { - Path: "/virtualmachines-mutate", - Group: "kubevirt.io", - Resource: "virtualmachines", - }, - "/virtualmachineinstances-mutate": { - Path: "/virtualmachineinstances-mutate", - Group: "kubevirt.io", - Resource: "virtualmachineinstances", - }, - "/migration-mutate-create": { - Path: "/migration-mutate-create", - Group: "kubevirt.io", - Resource: "virtualmachineinstancemigrations", - }, - "/vm-clone-mutate-create": { - Path: "/vm-clone-mutate-create", - Group: "clone.kubevirt.io", - Resource: "virtualmachineclones", - }, -} diff --git a/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go new file mode 100644 index 0000000..dd85d8d --- /dev/null +++ b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go @@ -0,0 +1,158 @@ +/* +Copyright 2024 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 operatornelm + +import ( + . "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" +) + +const ( + internalPrefix = "internal.operator-helm.deckhouse.io" +) + +var OperatorNelmRewriteRules = &RewriteRules{ + KindPrefix: "InternalNelmOperator", + ResourceTypePrefix: "internalnelmoperator", + ShortNamePrefix: "intnelm", + Categories: []string{"intnelm"}, + Rules: OperatorNelmAPIGroupsRules, + Webhooks: OperatorNelmWebhooks, + Labels: MetadataReplace{ + Names: []MetadataReplaceRule{ + {Original: "source.werf.io", Renamed: "source." + internalPrefix}, + {Original: "helm.werf.io", Renamed: "helm." + internalPrefix}, + }, + Prefixes: []MetadataReplaceRule{ + {Original: "source.werf.io", Renamed: "source." + internalPrefix}, + {Original: "helm.werf.io", Renamed: "helm." + internalPrefix}, + }, + }, + Annotations: MetadataReplace{ + Prefixes: []MetadataReplaceRule{ + {Original: "source.werf.io", Renamed: "source." + internalPrefix}, + {Original: "helm.werf.io", Renamed: "helm." + internalPrefix}, + }, + }, + Finalizers: MetadataReplace{ + Names: []MetadataReplaceRule{ + {Original: "finalizers.werf.io", Renamed: "finalizers." + internalPrefix}, + }, + Prefixes: []MetadataReplaceRule{ + {Original: "werf.io", Renamed: "werf." + internalPrefix}, + }, + }, + Excludes: []ExcludeRule{}, + KindRefPaths: map[string][]string{ + "HelmChart": {"spec.sourceRef"}, + "HelmRelease": {"spec.chart.spec.sourceRef", "spec.chartRef"}, + }, +} + +var OperatorNelmAPIGroupsRules = map[string]APIGroupRule{ + "source.werf.io": { + GroupRule: GroupRule{ + Group: "source.werf.io", + Versions: []string{"v1beta1", "v1beta2", "v1"}, + PreferredVersion: "v1", + Renamed: "source." + internalPrefix, + }, + ResourceRules: map[string]ResourceRule{ + "buckets": { + Kind: "Bucket", + ListKind: "BucketList", + Plural: "buckets", + Singular: "bucket", + Versions: []string{"v1beta2", "v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{}, + }, + "externalartifacts": { + Kind: "ExternalArtifact", + ListKind: "ExternalArtifactList", + Plural: "externalartifacts", + Singular: "externalartifact", + Versions: []string{"v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{}, + }, + "gitrepositories": { + Kind: "GitRepository", + ListKind: "GitRepositoryList", + Plural: "gitrepositories", + Singular: "gitrepository", + Versions: []string{"v1beta2", "v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{"gitrepo"}, + }, + "helmcharts": { + Kind: "HelmChart", + ListKind: "HelmChartList", + Plural: "helmcharts", + Singular: "helmchart", + Versions: []string{"v1beta2", "v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{"hc"}, + }, + "helmrepositories": { + Kind: "HelmRepository", + ListKind: "HelmRepositoryList", + Plural: "helmrepositories", + Singular: "helmrepository", + Versions: []string{"v1beta2", "v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{"helmrepo"}, + }, + "ocirepositories": { + Kind: "OCIRepository", + ListKind: "OCIRepositoryList", + Plural: "ocirepositories", + Singular: "ocirepository", + Versions: []string{"v1beta2", "v1"}, + PreferredVersion: "v1", + Categories: []string{}, + ShortNames: []string{"ocirepo"}, + }, + }, + }, + "helm.werf.io": { + GroupRule: GroupRule{ + Group: "helm.werf.io", + Versions: []string{"v2beta1", "v2beta2", "v2"}, + PreferredVersion: "v2", + Renamed: "helm." + internalPrefix, + }, + ResourceRules: map[string]ResourceRule{ + "helmreleases": { + Kind: "HelmRelease", + ListKind: "HelmReleaseList", + Plural: "helmreleases", + Singular: "helmrelease", + Versions: []string{"v2beta2", "v2"}, + PreferredVersion: "v2", + Categories: []string{}, + ShortNames: []string{"hr"}, + }, + }, + }, +} + +var OperatorNelmWebhooks = map[string]WebhookRule{} diff --git a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules_test.go similarity index 77% rename from images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go rename to images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules_test.go index 16698bb..876ed3f 100644 --- a/images/kube-api-rewriter/pkg/kubevirt/kubevirt_rules_test.go +++ b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules_test.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package kubevirt +package operatornelm import ( "fmt" @@ -23,10 +23,10 @@ import ( "sigs.k8s.io/yaml" ) -func TestKubevirtRulesToYAML(t *testing.T) { - b, err := yaml.Marshal(KubevirtRewriteRules) +func TestOperatorNelmRulesToYAML(t *testing.T) { + b, err := yaml.Marshal(OperatorNelmRewriteRules) if err != nil { - t.Fatalf("should marshal kubevirt rules without error: %v", err) + t.Fatalf("should marshal operatornelm rules without error: %v", err) } fmt.Printf("%s\n", string(b)) diff --git a/images/kube-api-rewriter/pkg/proxy/handler.go b/images/kube-api-rewriter/pkg/proxy/handler.go index a9dcb12..72c1dc6 100644 --- a/images/kube-api-rewriter/pkg/proxy/handler.go +++ b/images/kube-api-rewriter/pkg/proxy/handler.go @@ -303,8 +303,10 @@ func (h *Handler) transformRequest(targetReq *rewriter.TargetRequest, req *http. // Rewrite incoming payload, e.g. create, put, etc. if targetReq.ShouldRewriteRequest() && hasPayload { - switch req.Method { - case http.MethodPatch: + switch { + case req.Method == http.MethodPatch && isServerSideApply(req): + rwrBodyBytes, err = h.Rewriter.RewriteJSONPayload(targetReq, origBodyBytes, ToTargetAction(h.ProxyMode)) + case req.Method == http.MethodPatch: rwrBodyBytes, err = h.Rewriter.RewritePatch(targetReq, origBodyBytes) default: rwrBodyBytes, err = h.Rewriter.RewriteJSONPayload(targetReq, origBodyBytes, ToTargetAction(h.ProxyMode)) @@ -329,32 +331,43 @@ func (h *Handler) transformRequest(targetReq *rewriter.TargetRequest, req *http. if targetReq.ShouldRewriteResponse() { newAccept := make([]string, 0) for _, hdr := range req.Header.Values("Accept") { - // Rewriter doesn't work with protobuf, force JSON in Accept header. - // This workaround is suitable only for empty body requests: Get, List, etc. - // A client should be patched to send JSON requests. - if strings.Contains(hdr, "application/vnd.kubernetes.protobuf") { - newAccept = append(newAccept, "application/json") - continue + // Accept header may contain comma-separated media types + // (e.g. "application/vnd.kubernetes.protobuf;as=PartialObjectMetadata;...,application/json;as=PartialObjectMetadata;...,application/json"). + // Process each media type individually to avoid discarding + // non-protobuf alternatives when a protobuf entry is present. + mediaTypes := strings.Split(hdr, ",") + filteredTypes := make([]string, 0, len(mediaTypes)) + for _, mt := range mediaTypes { + mt = strings.TrimSpace(mt) + if mt == "" { + continue + } + + // Rewriter doesn't work with protobuf, drop protobuf media types. + if strings.Contains(mt, "application/vnd.kubernetes.protobuf") { + continue + } + + // TODO Add rewriting support for Table format. + // Quickly support kubectl with simple hack + if strings.Contains(mt, "application/json") && strings.Contains(mt, "as=Table") { + filteredTypes = append(filteredTypes, "application/json") + continue + } + + filteredTypes = append(filteredTypes, mt) } - - // TODO Add rewriting support for Table format. - // Quickly support kubectl with simple hack - if strings.Contains(hdr, "application/json") && strings.Contains(hdr, "as=Table") { - newAccept = append(newAccept, "application/json") - continue + if len(filteredTypes) > 0 { + newAccept = append(newAccept, strings.Join(filteredTypes, ",")) } + } - newAccept = append(newAccept, hdr) + // Ensure Accept is not empty: fall back to application/json. + if len(newAccept) == 0 { + newAccept = append(newAccept, "application/json") } req.Header["Accept"] = newAccept - - // Force JSON for watches of core resources and CRDs. - if targetReq.IsWatch() && (targetReq.IsCRD() || targetReq.IsCore()) { - if len(req.Header.Values("Accept")) == 0 { - req.Header["Accept"] = []string{"application/json"} - } - } } // Set new endpoint path and query. @@ -529,6 +542,15 @@ func (iw *immediateWriter) Write(p []byte) (n int, err error) { return } +// isServerSideApply returns true if the request is a server-side apply patch. +// Server-side apply uses Content-Type "application/apply-patch+yaml" and sends +// a full resource manifest (including apiVersion and kind), unlike regular +// merge/JSON patches that only contain partial updates. +func isServerSideApply(req *http.Request) bool { + ct := req.Header.Get("Content-Type") + return strings.Contains(ct, "application/apply-patch") +} + // notFoundJSON constructs Status response of type NotFound // for resourceType and object name. // Example: diff --git a/images/kube-api-rewriter/pkg/proxy/handler_test.go b/images/kube-api-rewriter/pkg/proxy/handler_test.go deleted file mode 100644 index 265d4d5..0000000 --- a/images/kube-api-rewriter/pkg/proxy/handler_test.go +++ /dev/null @@ -1,778 +0,0 @@ -/* -Copyright 2024 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 proxy - -import ( - "bytes" - "fmt" - "io" - "net/http" - "net/http/pprof" - "net/url" - "runtime" - "sort" - "strconv" - "strings" - "sync" - "testing" - "time" - - "github.com/tidwall/gjson" - - "github.com/deckhouse/kube-api-rewriter/pkg/kubevirt" - "github.com/deckhouse/kube-api-rewriter/pkg/log" - "github.com/deckhouse/kube-api-rewriter/pkg/rewriter" - "github.com/deckhouse/kube-api-rewriter/pkg/server" -) - -// PodJSON is a real Pod example to test JSON rewrites. -const PodJSON = `{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "example-pod", - "annotations": { - "cni.cilium.io/ipAddress": "10.66.10.1", - "kubectl.kubernetes.io/default-container": "compute", - "kubevirt.internal.virtualization.deckhouse.io/allow-pod-bridge-network-live-migration": "true", - "kubevirt.internal.virtualization.deckhouse.io/domain": "cloud-alpine", - "kubevirt.internal.virtualization.deckhouse.io/migrationTransportUnix": "true", - "kubevirt.internal.virtualization.deckhouse.io/vm-generation": "1", - "post.hook.backup.velero.io/command": "[\"/usr/bin/virt-freezer\", \"--unfreeze\", \"--name\", \"cloud-alpine\", \"--namespace\", \"vm\"]", - "post.hook.backup.velero.io/container": "compute", - "pre.hook.backup.velero.io/command": "[\"/usr/bin/virt-freezer\", \"--freeze\", \"--name\", \"cloud-alpine\", \"--namespace\", \"vm\"]", - "pre.hook.backup.velero.io/container": "compute" - }, - "creationTimestamp": "2024-10-01T11:45:59Z", - "finalizers": [ - "virtualization.deckhouse.io/pod-protection" - ], - "generateName": "virt-launcher-cloud-alpine-", - "labels": { - "kubevirt.internal.virtualization.deckhouse.io": "virt-launcher", - "kubevirt.internal.virtualization.deckhouse.io/created-by": "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f", - "kubevirt.internal.virtualization.deckhouse.io/nodeName": "virtlab-delivery-mi-2", - "vm": "cloud-alpine", - "vm-folder": "vm-cloud-alpine", - "vm.kubevirt.internal.virtualization.deckhouse.io/name": "cloud-alpine" - }, - "name": "virt-launcher-cloud-alpine-lxlz5", - "namespace": "vm", - "ownerReferences": [ - { - "apiVersion": "internal.virtualization.deckhouse.io/v1", - "blockOwnerDeletion": true, - "controller": true, - "kind": "InternalVirtualizationVirtualMachineInstance", - "name": "cloud-alpine", - "uid": "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f" - } - ], - "resourceVersion": "595346645", - "uid": "68558c6e-aefb-4cbb-922a-e8389e8ce43f" - }, - "spec": { - "affinity": { - "nodeAffinity": { - "requiredDuringSchedulingIgnoredDuringExecution": { - "nodeSelectorTerms": [ - { - "matchExpressions": [ - { - "key": "node-role.kubernetes.io/control-plane", - "operator": "DoesNotExist" - } - ] - } - ] - } - } - }, - "automountServiceAccountToken": false, - "containers": [ - { - "command": [ - "/usr/bin/virt-launcher-monitor", - "--qemu-timeout", - "338s", - "--name", - "cloud-alpine", - "--uid", - "ac1e83d8-f2ad-4047-8ba9-3f557c687b9f", - "--namespace", - "vm", - "--kubevirt-share-dir", - "/var/run/kubevirt", - "--ephemeral-disk-dir", - "/var/run/kubevirt-ephemeral-disks", - "--container-disk-dir", - "/var/run/kubevirt/container-disks", - "--grace-period-seconds", - "75", - "--hook-sidecars", - "0", - "--ovmf-path", - "/usr/share/OVMF" - ], - "env": [ - { - "name": "POD_NAME", - "valueFrom": { - "fieldRef": { - "apiVersion": "v1", - "fieldPath": "metadata.name" - } - } - } - ], - "image": "dev-registry.deckhouse.io/sys/deckhouse-oss/modules/virtualization@sha256:c3c6c6a87ce0082697da80a6e53b4bf59fb433be05cabd9f7c46201bd45283e6", - "imagePullPolicy": "IfNotPresent", - "name": "compute", - "resources": { - "limits": { - "cpu": "4", - "devices.virtualization.deckhouse.io/kvm": "1", - "devices.virtualization.deckhouse.io/tun": "1", - "devices.virtualization.deckhouse.io/vhost-net": "1", - "memory": "4582277121" - }, - "requests": { - "cpu": "4", - "devices.virtualization.deckhouse.io/kvm": "1", - "devices.virtualization.deckhouse.io/tun": "1", - "devices.virtualization.deckhouse.io/vhost-net": "1", - "ephemeral-storage": "50M", - "memory": "4582277121" - } - }, - "securityContext": { - "capabilities": { - "add": [ - "NET_BIND_SERVICE", - "SYS_NICE" - ] - }, - "privileged": false, - "runAsNonRoot": false, - "runAsUser": 0 - }, - "terminationMessagePath": "/dev/termination-log", - "terminationMessagePolicy": "File", - "volumeDevices": [ - { - "devicePath": "/dev/vd-cloud-alpine", - "name": "vd-cloud-alpine" - }, - { - "devicePath": "/dev/vd-cloud-alpine-data", - "name": "vd-cloud-alpine-data" - } - ], - "volumeMounts": [ - { - "mountPath": "/var/run/kubevirt-private", - "name": "private" - }, - { - "mountPath": "/var/run/kubevirt", - "name": "public" - }, - { - "mountPath": "/var/run/kubevirt-ephemeral-disks", - "name": "ephemeral-disks" - }, - { - "mountPath": "/var/run/kubevirt/container-disks", - "mountPropagation": "HostToContainer", - "name": "container-disks" - }, - { - "mountPath": "/var/run/libvirt", - "name": "libvirt-runtime" - }, - { - "mountPath": "/var/run/kubevirt/sockets", - "name": "sockets" - }, - { - "mountPath": "/var/run/kubevirt/hotplug-disks", - "mountPropagation": "HostToContainer", - "name": "hotplug-disks" - } - ] - } - ], - - "dnsPolicy": "ClusterFirst", - "enableServiceLinks": false, - "hostname": "cloud-alpine", - "nodeName": "virtlab-delivery-mi-2", - "nodeSelector": { - "cpu-model.node.virtualization.deckhouse.io/Nehalem": "true", - "kubernetes.io/arch": "amd64", - "kubevirt.internal.virtualization.deckhouse.io/schedulable": "true" - }, - "preemptionPolicy": "PreemptLowerPriority", - "priority": 1000, - "priorityClassName": "develop", - "readinessGates": [ - { - "conditionType": "kubevirt.io/virtual-machine-unpaused" - } - ], - "restartPolicy": "Never", - "schedulerName": "linstor", - "securityContext": { - "runAsUser": 0 - }, - "serviceAccount": "default", - "serviceAccountName": "default", - "terminationGracePeriodSeconds": 90, - - "tolerations": [ - { - "effect": "NoExecute", - "key": "node.kubernetes.io/not-ready", - "operator": "Exists", - "tolerationSeconds": 300 - }, - { - "effect": "NoExecute", - "key": "node.kubernetes.io/unreachable", - "operator": "Exists", - "tolerationSeconds": 300 - }, - { - "effect": "NoSchedule", - "key": "node.kubernetes.io/memory-pressure", - "operator": "Exists" - }, - { - "effect": "NoSchedule", - "key": "devices.virtualization.deckhouse.io/kvm", - "operator": "Exists" - }, - { - "effect": "NoSchedule", - "key": "devices.virtualization.deckhouse.io/tun", - "operator": "Exists" - }, - { - "effect": "NoSchedule", - "key": "devices.virtualization.deckhouse.io/vhost-net", - "operator": "Exists" - } - ], - - "volumes": [ - { - "emptyDir": {}, - "name": "private" - }, - { - "emptyDir": {}, - "name": "public" - }, - { - "emptyDir": {}, - "name": "sockets" - }, - { - "emptyDir": {}, - "name": "virt-bin-share-dir" - }, - { - "emptyDir": {}, - "name": "libvirt-runtime" - }, - { - "emptyDir": {}, - "name": "ephemeral-disks" - }, - { - "emptyDir": {}, - "name": "container-disks" - }, - { - "name": "vd-cloud-alpine", - "persistentVolumeClaim": { - "claimName": "vd-cloud-alpine-30e0ce5d-d0d7-4f38-b0a2-493330e5bb4a" - } - }, - { - "name": "vd-cloud-alpine-data", - "persistentVolumeClaim": { - "claimName": "vd-cloud-alpine-data-23941f64-7241-40a1-8fc1-f976c7c364e8" - } - }, - { - "emptyDir": {}, - "name": "hotplug-disks" - } - ] - }, - "status": { - "conditions": [ - { - "lastProbeTime": null, - "lastTransitionTime": null, - "status": "False", - "type": "Custom" - }, - { - "lastProbeTime": "2024-10-01T11:45:59Z", - "lastTransitionTime": "2024-10-01T11:45:59Z", - "message": "the virtual machine is not paused", - "reason": "NotPaused", - "status": "True", - "type": "kubevirt.io/virtual-machine-unpaused" - }, - { - "lastProbeTime": null, - "lastTransitionTime": "2024-10-01T11:45:59Z", - "status": "True", - "type": "Initialized" - }, - { - "lastProbeTime": null, - "lastTransitionTime": "2024-10-01T11:46:01Z", - "status": "True", - "type": "Ready" - }, - { - "lastProbeTime": null, - "lastTransitionTime": "2024-10-01T11:46:01Z", - "status": "True", - "type": "ContainersReady" - }, - { - "lastProbeTime": null, - "lastTransitionTime": "2024-10-01T11:45:59Z", - "status": "True", - "type": "PodScheduled" - } - ], - "containerStatuses": [ - { - "containerID": "containerd://4305d5ef79c16cbb9f28450506f9ec4650269e8034bdd0d5d42189aa638effb4", - "image": "sha256:cf321ffda57daa4fbf19daf047506fd36a841ced39ef869a80cc53a6387bba26", - "imageID": "dev-registry.deckhouse.io/sys/deckhouse-oss/modules/virtualization@sha256:c3c6c6a87ce0082697da80a6e53b4bf59fb433be05cabd9f7c46201bd45283e6", - "lastState": {}, - "name": "compute", - "ready": true, - "restartCount": 0, - "started": true, - "state": { - "running": { - "startedAt": "2024-10-01T11:46:01Z" - } - } - } - ], - "hostIP": "172.18.18.72", - "phase": "Running", - "podIP": "10.66.10.1", - "podIPs": [ - { - "ip": "10.66.10.1" - } - ], - "qosClass": "Guaranteed", - "startTime": "2024-10-01T11:45:59Z" - } -}` - -// Test_run_proxy_with_pprof runs server, rewriter and a client -// in different go routines for experimenting with pprof. -// -// Start test and run go tool: -// -// go tool pprof -http=127.0.0.1:8085 http://127.0.0.1:43200/debug/pprof/heap -func Test_run_proxy_with_pprof(t *testing.T) { - // Comment to run experiments. - t.SkipNow() - - // Memory stats printer. - go func() { - ticker := time.NewTicker(3 * time.Second) - for { - <-ticker.C - var stats runtime.MemStats - runtime.ReadMemStats(&stats) - fmt.Printf( - "Heap Alloc: %0.2f MB, Heap InUse %0.2f MB\n", - float64(stats.HeapAlloc)/1024/1024, - float64(stats.HeapInuse)/1024/1024, - ) - } - }() - - // Pprof server - go func() { - mux := http.NewServeMux() - - mux.HandleFunc("/debug/pprof/", pprof.Index) - mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - mux.HandleFunc("/debug/pprof/profile", pprof.Profile) - mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - mux.HandleFunc("/debug/pprof/trace", pprof.Trace) - - pprofSrv := &http.Server{ - Addr: "127.0.0.1:43200", - Handler: mux, - } - - fmt.Println("Pprof server started at 127.0.0.1:43200") - if err := pprofSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - fmt.Println("Error starting pprof server:", err) - } - }() - - // This HTTP server implements List Pods endpoint of the Kubernetes API Server. - kubeAPIRready := make(chan struct{}, 0) - // Change count to stress test the rewriter. - podsCount := 3200 - go func() { - items := strings.Repeat(PodJSON+",", podsCount-1) - PodsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` - - once := 0 - - handleGet := func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", strconv.Itoa(len(PodsListJSON))) - w.WriteHeader(http.StatusOK) - wrbytes, err := io.Copy(w, bytes.NewBuffer([]byte(PodsListJSON))) - if err != nil { - t.Fatalf("Should send pod list: %v", err) - } - - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - - if once == 0 { - fmt.Printf("pods requested, send %d bytes (%d written)\n", len(PodsListJSON), wrbytes) - once = 1 - } - } - - handleRequest := func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - handleGet(w, r) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - } - - mux := http.NewServeMux() - mux.HandleFunc("/api/v1/namespaces/vm/pods", handleRequest) - - kubeAPISrv := &http.Server{ - Addr: "127.0.0.1:43215", - Handler: mux, - } - - fmt.Println("Server started at 127.0.0.1:43215") - close(kubeAPIRready) - if err := kubeAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - fmt.Println("Error starting server:", err) - } - }() - - // This HTTP server runs the rewriter. Switch client to use it to detect problems with proxy handler. - go func() { - items := strings.Repeat(PodJSON+",", podsCount-1) - PodsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` - - rewriteRules := kubevirt.KubevirtRewriteRules - rewriteRules.Init() - - rwr := &rewriter.RuleBasedRewriter{ - Rules: rewriteRules, - } - - once := 0 - - handleGet := func(w http.ResponseWriter, r *http.Request) { - rwrBytes, err := rwr.RewriteJSONPayload(nil, []byte(PodsListJSON), rewriter.Rename) - if err != nil { - t.Fatalf("Should rewrite JSON pod list: %v", err) - return - } - - w.Header().Set("Content-Type", "application/json") - w.Header().Set("Content-Length", strconv.Itoa(len(rwrBytes))) - w.WriteHeader(http.StatusOK) - wrbytes, err := io.Copy(w, bytes.NewBuffer(rwrBytes)) - if err != nil { - t.Fatalf("Should send pod list: %v", err) - } - - if flusher, ok := w.(http.Flusher); ok { - flusher.Flush() - } - - if once == 0 { - fmt.Printf("pods requested, send %d bytes (%d written)\n", len(PodsListJSON), wrbytes) - once = 1 - } - } - - handleRequest := func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - handleGet(w, r) - default: - http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) - } - } - - mux := http.NewServeMux() - mux.HandleFunc("/api/v1/namespaces/vm/pods", handleRequest) - - kubeAPISrv := &http.Server{ - Addr: "127.0.0.1:43217", - Handler: mux, - } - - fmt.Println("Server started at 127.0.0.1:43217") - if err := kubeAPISrv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - fmt.Println("Error starting server:", err) - } - }() - - // A rewriter proxy. - go func() { - log.SetupDefaultLoggerFromEnv(log.Options{ - Level: "debug", - Format: "pretty", - Output: "discard", - }) - //slog.SetDefault(slog.New(slog.NewTextHandler(io.Discard, nil))) - - apiServerURL := "http://127.0.0.1:43215" - targetURL, err := url.Parse(apiServerURL) - if err != nil { - t.Fatalf("Should parse url %s: %v", apiServerURL, err) - return - } - - rewriteRules := kubevirt.KubevirtRewriteRules - rewriteRules.Init() - - rwr := &rewriter.RuleBasedRewriter{ - Rules: rewriteRules, - } - proxyHandler := &Handler{ - Name: "test-mem-leak", - TargetClient: &http.Client{}, - TargetURL: targetURL, - ProxyMode: ToRenamed, - Rewriter: rwr, - } - - srv := &server.HTTPServer{ - InstanceDesc: "Test Mem Leak", - ListenAddr: "127.0.0.1:43216", - RootHandler: proxyHandler, - } - - srv.Start() - }() - - <-kubeAPIRready - - fmt.Println("Start spamming ...") - - // Spam proxy with requests. - start := time.Now() - spamDuration := time.Minute - sleepDuration := time.Minute - //maxCount := 2200000 - count := 1 - for { - // Choose what source to test. - // No proxy, no rewrites. - // req, err := http.NewRequest("GET", "http://127.0.0.1:43215/api/v1/namespaces/vm/pods", nil) - // No proxy, only rewriter. - req, err := http.NewRequest("GET", "http://127.0.0.1:43217/api/v1/namespaces/vm/pods", nil) - // Proxy and rewriter. - // req, err := http.NewRequest("GET", "http://127.0.0.1:43216/api/v1/namespaces/vm/pods", nil) - if err != nil { - t.Fatalf("Should not fail on creating request %d: %v", count, err) - return - } - - startRequest := time.Now() - - resp, err := http.DefaultClient.Do(req) - - if err != nil { - t.Fatalf("Should not fail on GET request %d: %v", count, err) - return - } - - startRead := time.Now() - - podBytes, err := io.ReadAll(resp.Body) - if err != nil { - t.Fatalf("Should not fail on reading response %d: %v", count, err) - return - } - endRead := time.Now() - - resp.Body.Close() - - respKind := gjson.GetBytes(podBytes, "kind").String() - if respKind != "PodList" { - t.Fatalf("Got unexpected kind: %s", respKind) - return - } - - endKind := time.Now() - - if count == 1 { - dur := endRead.Sub(startRequest) - speed := float64(len(podBytes)) / dur.Seconds() / 1024 - fmt.Printf("Request time: %s, Speed: %0.2f kb/s\n", startRead.Sub(startRequest).Truncate(time.Millisecond).String(), speed) - fmt.Printf("Read time: %s, Speed: %0.2f kb/s\n", endRead.Sub(startRead).Truncate(time.Millisecond).String(), speed) - fmt.Printf("Whole time: %s\n", endKind.Sub(startRequest).Truncate(time.Millisecond).String()) - fmt.Printf("%d. Got %s. Read %d bytes.\n", count, respKind, len(podBytes)) - } - - now := time.Now() - if now.Sub(start) > spamDuration { - fmt.Printf("Send %d requests in %s\n", count, now.Sub(start).Truncate(time.Second).String()) - break - } - - podBytes = nil - - count++ - //if count == maxCount { - // return - //} - } - - time.Sleep(sleepDuration) -} - -// Test_RewriteJSONPayload_time runs RewriteJSONPayload -// with different PodList lengths and outputs time stats. -// -// Example: -// -// === RUN Test_RewriteJSONPayload_time -// Got 9 results -// 100 expect: 1.78s got: 1.78s x1.00 -// 200 expect: 3.56s got: 1.875s x0.53 -// 400 expect: 7.12s got: 2.39s x0.34 -// 800 expect: 14.24s got: 3.83s x0.27 -// 1600 expect: 28.48s got: 4.709s x0.17 -// 3200 expect: 56.96s got: 6.077s x0.11 -// 6400 expect: 1m53.921s got: 8.396s x0.07 -// 12800 expect: 3m47.842s got: 12.013s x0.05 -// 25600 expect: 7m35.685s got: 17.271s x0.04 -func Test_RewriteJSONPayload_time(t *testing.T) { - t.SkipNow() - - rewriteRules := kubevirt.KubevirtRewriteRules - rewriteRules.Init() - - rwr := &rewriter.RuleBasedRewriter{ - Rules: rewriteRules, - } - - podListCounts := []int{ - 100, - 200, - 400, - 800, - 1600, - 3200, - 6400, - 12800, - 25600, - } - - var wg sync.WaitGroup - wg.Add(len(podListCounts)) - - type testRes struct { - count int - execDur time.Duration - bytesCount int - rwrBytesCount int - } - - resCh := make(chan testRes, len(podListCounts)) - - for _, podListCount := range podListCounts { - go func(podsCount int) { - // Construct PodList with podsCount items. Name uniqueness - // is not significant for the test purposes. - items := strings.Repeat(PodJSON+",", podsCount-1) - podsListJSON := `{"apiVersion":"v1", "kind":"PodList", "items":[` + items + PodJSON + `]}` - - start := time.Now() - rwrBytes, err := rwr.RewriteJSONPayload(nil, []byte(podsListJSON), rewriter.Restore) - if err != nil { - t.Fatalf("Should rewrite JSON: %v", err) - return - } - end := time.Now() - - resCh <- testRes{ - count: podsCount, - execDur: end.Sub(start), - bytesCount: len(podsListJSON), - rwrBytesCount: len(rwrBytes), - } - - wg.Done() - }(podListCount) - } - - wg.Wait() - - // Extract results from the chan. - testResults := make([]testRes, 0, len(podListCounts)) - for range podListCounts { - res := <-resCh - testResults = append(testResults, res) - } - - // Print sorted results. - fmt.Printf("Got %d results\n", len(testResults)) - sort.SliceStable(testResults, func(i, j int) bool { - return testResults[i].count < testResults[j].count - }) - first := testResults[0] - for _, res := range testResults { - expectedDur := time.Duration(res.count/first.count) * first.execDur - ratio := float64(res.execDur) / float64(expectedDur) - - fmt.Printf("%5d expect: %10s got: %10s x%0.2f\n", - res.count, - expectedDur.Truncate(time.Millisecond).String(), - res.execDur.Truncate(time.Millisecond).String(), - ratio, - ) - } -} diff --git a/images/kube-api-rewriter/pkg/proxy/stream_handler.go b/images/kube-api-rewriter/pkg/proxy/stream_handler.go index 7d44dc3..f599ce6 100644 --- a/images/kube-api-rewriter/pkg/proxy/stream_handler.go +++ b/images/kube-api-rewriter/pkg/proxy/stream_handler.go @@ -122,7 +122,7 @@ func (s *streamRewriter) start(ctx context.Context) { res, _, err := s.decoder.Decode(nil, &got) s.metrics.FromTargetBytesAdd(CounterValue(s.bytesCounter)) if s.log.Enabled(ctx, slog.LevelDebug) { - s.log.Debug("Got decoded WatchEvent from stream: %d bytes received", CounterValue(s.bytesCounter)) + s.log.Debug(fmt.Sprintf("Got decoded WatchEvent from stream: %d bytes received", CounterValue(s.bytesCounter))) } CounterReset(s.bytesCounter) diff --git a/images/kube-api-rewriter/pkg/rewriter/resource.go b/images/kube-api-rewriter/pkg/rewriter/resource.go index e09c2de..50693bb 100644 --- a/images/kube-api-rewriter/pkg/rewriter/resource.go +++ b/images/kube-api-rewriter/pkg/rewriter/resource.go @@ -157,5 +157,9 @@ func RenameManagedFields(rules *RewriteRules, obj []byte) ([]byte, error) { } func RenameResourcePatch(rules *RewriteRules, patch []byte) ([]byte, error) { + patch, err := RewritePatchSourceRefs(rules, patch) + if err != nil { + return nil, err + } return RenameMetadataPatch(rules, patch) } diff --git a/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go index ba17eea..e9bd4be 100644 --- a/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go +++ b/images/kube-api-rewriter/pkg/rewriter/rule_rewriter.go @@ -307,8 +307,13 @@ func (rw *RuleBasedRewriter) RewriteJSONPayload(_ *TargetRequest, obj []byte, ac } // Always rewrite metadata: labels, annotations, finalizers, ownerReferences. + // Also rewrite spec-level kind references (e.g. spec.sourceRef.kind in HelmChart). // TODO: add rewriter for managedFields. return RewriteResourceOrList2(rwrBytes, func(singleObj []byte) ([]byte, error) { + singleObj, err = RewriteSpecKindRefs(rw.Rules, singleObj, action) + if err != nil { + return nil, err + } return TransformObject(singleObj, "metadata", func(metadataObj []byte) ([]byte, error) { return RewriteMetadata(rw.Rules, metadataObj, action) }) diff --git a/images/kube-api-rewriter/pkg/rewriter/rules.go b/images/kube-api-rewriter/pkg/rewriter/rules.go index 8e97333..f03265b 100644 --- a/images/kube-api-rewriter/pkg/rewriter/rules.go +++ b/images/kube-api-rewriter/pkg/rewriter/rules.go @@ -36,6 +36,11 @@ type RewriteRules struct { Finalizers MetadataReplace `json:"finalizers"` Excludes []ExcludeRule `json:"excludes"` + // KindRefPaths maps original Kind names to spec-level JSON paths that + // contain kind references (e.g. sourceRef). This drives data-driven + // rewriting of cross-resource kind fields instead of hardcoding them. + KindRefPaths map[string][]string `json:"kindRefPaths"` + // TODO move these indexed rewriters into the RuleBasedRewriter. labelsRewriter *PrefixedNameRewriter annotationsRewriter *PrefixedNameRewriter @@ -403,3 +408,31 @@ func mapContainsMap(obj, match map[string]string) bool { } return true } + +// KindRefPathsFor returns the spec-level JSON paths containing kind references +// for the given original Kind name. Returns nil if no paths are configured. +func (rr *RewriteRules) KindRefPathsFor(origKind string) []string { + if rr.KindRefPaths == nil { + return nil + } + return rr.KindRefPaths[origKind] +} + +// AllKindRefPaths returns a deduplicated union of all spec-level JSON paths +// across all kinds. Returns nil if no paths are configured. +func (rr *RewriteRules) AllKindRefPaths() []string { + if len(rr.KindRefPaths) == 0 { + return nil + } + seen := make(map[string]struct{}) + var result []string + for _, paths := range rr.KindRefPaths { + for _, p := range paths { + if _, ok := seen[p]; !ok { + seen[p] = struct{}{} + result = append(result, p) + } + } + } + return result +} diff --git a/images/kube-api-rewriter/pkg/rewriter/source_ref.go b/images/kube-api-rewriter/pkg/rewriter/source_ref.go new file mode 100644 index 0000000..54eb63b --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/source_ref.go @@ -0,0 +1,109 @@ +/* +Copyright 2024 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 rewriter + +import ( + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// RewriteKindRef rewrites the "kind" field in an object that references another +// resource kind (e.g., spec.sourceRef in HelmChart). If "apiVersion" is also +// present, both fields are rewritten using RewriteAPIVersionAndKind. +func RewriteKindRef(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + kind := gjson.GetBytes(obj, "kind").String() + if kind == "" { + return obj, nil + } + + apiVersion := gjson.GetBytes(obj, "apiVersion").String() + if apiVersion != "" { + return RewriteAPIVersionAndKind(rules, obj, action) + } + + var rwrKind string + if action == Rename { + _, resRule := rules.GroupResourceRulesByKind(kind) + if resRule == nil { + return obj, nil + } + rwrKind = rules.RenameKind(kind) + } + if action == Restore { + restoredKind := rules.RestoreKind(kind) + _, resRule := rules.GroupResourceRulesByKind(restoredKind) + if resRule == nil { + return obj, nil + } + rwrKind = restoredKind + } + + if rwrKind == "" || rwrKind == kind { + return obj, nil + } + + return sjson.SetBytes(obj, "kind", rwrKind) +} + +// RewriteSpecKindRefs rewrites kind references in spec fields of known resources. +// It uses KindRefPaths from rules to determine which spec paths contain kind +// references for each resource kind. +func RewriteSpecKindRefs(rules *RewriteRules, obj []byte, action Action) ([]byte, error) { + kind := gjson.GetBytes(obj, "kind").String() + origKind := rules.RestoreKind(kind) + + paths := rules.KindRefPathsFor(origKind) + if len(paths) == 0 { + return obj, nil + } + + var err error + for _, path := range paths { + obj, err = TransformObject(obj, path, func(refObj []byte) ([]byte, error) { + return RewriteKindRef(rules, refObj, action) + }) + if err != nil { + return nil, err + } + } + return obj, nil +} + +// RewritePatchSourceRefs rewrites sourceRef kind references in merge patches. +// It tries all configured KindRefPaths since merge patches do not have a +// top-level kind field to determine the resource type. +func RewritePatchSourceRefs(rules *RewriteRules, patch []byte) ([]byte, error) { + if len(patch) == 0 || patch[0] != '{' { + return patch, nil + } + + paths := rules.AllKindRefPaths() + if len(paths) == 0 { + return patch, nil + } + + var err error + for _, path := range paths { + patch, err = TransformObject(patch, path, func(refObj []byte) ([]byte, error) { + return RewriteKindRef(rules, refObj, Rename) + }) + if err != nil { + return nil, err + } + } + return patch, nil +} diff --git a/images/kube-api-rewriter/pkg/rewriter/source_ref_test.go b/images/kube-api-rewriter/pkg/rewriter/source_ref_test.go new file mode 100644 index 0000000..3897067 --- /dev/null +++ b/images/kube-api-rewriter/pkg/rewriter/source_ref_test.go @@ -0,0 +1,217 @@ +/* +Copyright 2024 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 rewriter + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +// testRulesWithKindRefPaths builds rules with custom kind names to prove +// data-driven behavior. Uses "SomeResource" and "OtherResource" (NOT +// "HelmChart"/"HelmRelease") so the hardcoded switch will NOT match. +func testRulesWithKindRefPaths() *RewriteRules { + rules := &RewriteRules{ + KindPrefix: "Prefixed", + ResourceTypePrefix: "prefixed", + ShortNamePrefix: "p", + Rules: map[string]APIGroupRule{ + "original.group.io": { + GroupRule: GroupRule{ + Group: "original.group.io", + Versions: []string{"v1"}, + PreferredVersion: "v1", + Renamed: "prefixed.resources.group.io", + }, + ResourceRules: map[string]ResourceRule{ + "someresources": { + Kind: "SomeResource", + ListKind: "SomeResourceList", + Plural: "someresources", + Singular: "someresource", + Versions: []string{"v1"}, + PreferredVersion: "v1", + }, + "otherresources": { + Kind: "OtherResource", + ListKind: "OtherResourceList", + Plural: "otherresources", + Singular: "otherresource", + Versions: []string{"v1"}, + PreferredVersion: "v1", + }, + }, + }, + }, + KindRefPaths: map[string][]string{ + "SomeResource": {"spec.sourceRef"}, + "OtherResource": {"spec.chart.spec.sourceRef", "spec.chartRef"}, + }, + } + rules.Init() + return rules +} + +// TestRewriteSpecKindRefs_RestoreKnownKind tests that Restore rewrites a renamed +// kind back to its original in spec.sourceRef for SomeResource. +func TestRewriteSpecKindRefs_RestoreKnownKind(t *testing.T) { + rules := testRulesWithKindRefPaths() + + // SomeResource has been renamed to PrefixedSomeResource. Its sourceRef + // contains a renamed kind that should be restored. + obj := []byte(`{"kind":"PrefixedSomeResource","spec":{"sourceRef":{"kind":"PrefixedSomeResource"}}}`) + + result, err := RewriteSpecKindRefs(rules, obj, Restore) + require.NoError(t, err) + + got := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "SomeResource", got, "sourceRef.kind should be restored to original") +} + +// TestRewriteSpecKindRefs_RenameKnownKind tests that Rename rewrites an original +// kind to the prefixed form in spec.sourceRef for SomeResource. +func TestRewriteSpecKindRefs_RenameKnownKind(t *testing.T) { + rules := testRulesWithKindRefPaths() + + // SomeResource (original kind) with sourceRef referencing another known kind. + obj := []byte(`{"kind":"SomeResource","spec":{"sourceRef":{"kind":"SomeResource"}}}`) + + result, err := RewriteSpecKindRefs(rules, obj, Rename) + require.NoError(t, err) + + got := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "PrefixedSomeResource", got, "sourceRef.kind should be renamed with prefix") +} + +// TestRewriteSpecKindRefs_RestoreMultiplePaths tests that OtherResource with two +// paths (spec.chart.spec.sourceRef and spec.chartRef) both get rewritten. +func TestRewriteSpecKindRefs_RestoreMultiplePaths(t *testing.T) { + rules := testRulesWithKindRefPaths() + + obj := []byte(`{ + "kind":"PrefixedOtherResource", + "spec":{ + "chart":{"spec":{"sourceRef":{"kind":"PrefixedSomeResource"}}}, + "chartRef":{"kind":"PrefixedOtherResource"} + } + }`) + + result, err := RewriteSpecKindRefs(rules, obj, Restore) + require.NoError(t, err) + + sourceRefKind := gjson.GetBytes(result, "spec.chart.spec.sourceRef.kind").String() + require.Equal(t, "SomeResource", sourceRefKind, "chart.spec.sourceRef.kind should be restored") + + chartRefKind := gjson.GetBytes(result, "spec.chartRef.kind").String() + require.Equal(t, "OtherResource", chartRefKind, "chartRef.kind should be restored") +} + +// TestRewriteSpecKindRefs_UnknownKindPassThrough tests that a kind not in +// KindRefPaths (e.g. ConfigMap) is returned unchanged. +func TestRewriteSpecKindRefs_UnknownKindPassThrough(t *testing.T) { + rules := testRulesWithKindRefPaths() + + obj := []byte(`{"kind":"ConfigMap","spec":{"sourceRef":{"kind":"SomeResource"}}}`) + + result, err := RewriteSpecKindRefs(rules, obj, Restore) + require.NoError(t, err) + + // sourceRef should be untouched since ConfigMap is not in KindRefPaths. + got := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "SomeResource", got, "unknown kind should pass through unchanged") +} + +// TestRewriteSpecKindRefs_NilKindRefPaths tests that nil KindRefPaths means +// all objects pass through unchanged. +func TestRewriteSpecKindRefs_NilKindRefPaths(t *testing.T) { + rules := testRulesWithKindRefPaths() + rules.KindRefPaths = nil + + obj := []byte(`{"kind":"PrefixedSomeResource","spec":{"sourceRef":{"kind":"PrefixedSomeResource"}}}`) + + result, err := RewriteSpecKindRefs(rules, obj, Restore) + require.NoError(t, err) + + // Should be unchanged since KindRefPaths is nil. + got := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "PrefixedSomeResource", got, "nil KindRefPaths should pass through") +} + +// TestRewritePatchSourceRefs_RewritesAllPaths tests that patches rewrite kind +// references across all configured paths. +func TestRewritePatchSourceRefs_RewritesAllPaths(t *testing.T) { + rules := testRulesWithKindRefPaths() + + patch := []byte(`{ + "spec":{ + "sourceRef":{"kind":"SomeResource"}, + "chart":{"spec":{"sourceRef":{"kind":"OtherResource"}}}, + "chartRef":{"kind":"SomeResource"} + } + }`) + + result, err := RewritePatchSourceRefs(rules, patch) + require.NoError(t, err) + + sourceRefKind := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "PrefixedSomeResource", sourceRefKind, "sourceRef.kind should be renamed") + + chartSourceRefKind := gjson.GetBytes(result, "spec.chart.spec.sourceRef.kind").String() + require.Equal(t, "PrefixedOtherResource", chartSourceRefKind, "chart.spec.sourceRef.kind should be renamed") + + chartRefKind := gjson.GetBytes(result, "spec.chartRef.kind").String() + require.Equal(t, "PrefixedSomeResource", chartRefKind, "chartRef.kind should be renamed") +} + +// TestRewritePatchSourceRefs_NilKindRefPaths tests that nil KindRefPaths means +// patches pass through unchanged. +func TestRewritePatchSourceRefs_NilKindRefPaths(t *testing.T) { + rules := testRulesWithKindRefPaths() + rules.KindRefPaths = nil + + patch := []byte(`{"spec":{"sourceRef":{"kind":"SomeResource"}}}`) + + result, err := RewritePatchSourceRefs(rules, patch) + require.NoError(t, err) + + got := gjson.GetBytes(result, "spec.sourceRef.kind").String() + require.Equal(t, "SomeResource", got, "nil KindRefPaths should pass through") +} + +// TestRewritePatchSourceRefs_EmptyPatch tests that empty input returns empty. +func TestRewritePatchSourceRefs_EmptyPatch(t *testing.T) { + rules := testRulesWithKindRefPaths() + + result, err := RewritePatchSourceRefs(rules, []byte{}) + require.NoError(t, err) + require.Empty(t, result) +} + +// TestRewritePatchSourceRefs_ArrayPatch tests that JSON array patches pass through. +func TestRewritePatchSourceRefs_ArrayPatch(t *testing.T) { + rules := testRulesWithKindRefPaths() + + patch := []byte(`[{"op":"replace","path":"/spec/sourceRef/kind","value":"SomeResource"}]`) + + result, err := RewritePatchSourceRefs(rules, patch) + require.NoError(t, err) + + // Array patches should pass through unchanged (they start with '[' not '{'). + require.Equal(t, string(patch), string(result)) +} diff --git a/images/kube-api-rewriter/werf.inc.yaml b/images/kube-api-rewriter/werf.inc.yaml index 3ff9afb..3a02526 100644 --- a/images/kube-api-rewriter/werf.inc.yaml +++ b/images/kube-api-rewriter/werf.inc.yaml @@ -13,7 +13,7 @@ git: --- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-builder final: false -fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.24" "builder/golang-alt-svace-1.24" }} +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} import: - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact add: /src @@ -38,9 +38,7 @@ shell: - | {{- $_ := set $ "ProjectName" (list $.ImageName "kube-api-rewriter" | join "/") }} {{- include "image-build.build" (set $ "BuildCommand" `go build -v -a -o kube-api-rewriter ./cmd/kube-api-rewriter`) | nindent 6 }} - --- - image: {{ .ModuleNamePrefix }}{{ .ImageName }} fromImage: builder/scratch git: From 6e4da42cfe1fe1e0241571fc58e9e513f5bcdf81 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 25 Feb 2026 11:21:01 +0300 Subject: [PATCH 03/26] feat: add operator-helm apis Signed-off-by: Ilya Drey --- .gitignore | 5 + api/Taskfile.dist.yaml | 41 + .../clientset/versioned/clientset.go | 120 + .../versioned/fake/clientset_generated.go | 105 + .../generated/clientset/versioned/fake/doc.go | 20 + .../clientset/versioned/fake/register.go | 56 + .../clientset/versioned/scheme/doc.go | 20 + .../clientset/versioned/scheme/register.go | 56 + .../typed/api/v1alpha1/api_client.go | 111 + .../versioned/typed/api/v1alpha1/doc.go | 20 + .../versioned/typed/api/v1alpha1/fake/doc.go | 20 + .../api/v1alpha1/fake/fake_api_client.go | 48 + .../v1alpha1/fake/fake_helmclusteraddon.go | 52 + .../fake/fake_helmclusteraddonchart.go | 52 + .../fake/fake_helmclusterrepository.go | 52 + .../typed/api/v1alpha1/generated_expansion.go | 25 + .../typed/api/v1alpha1/helmclusteraddon.go | 70 + .../api/v1alpha1/helmclusteraddonchart.go | 70 + .../api/v1alpha1/helmclusterrepository.go | 70 + .../externalversions/api/interface.go | 46 + .../api/v1alpha1/helmclusteraddon.go | 102 + .../api/v1alpha1/helmclusteraddonchart.go | 102 + .../api/v1alpha1/helmclusterrepository.go | 102 + .../api/v1alpha1/interface.go | 59 + .../informers/externalversions/factory.go | 263 ++ .../informers/externalversions/generic.go | 66 + .../internalinterfaces/factory_interfaces.go | 40 + .../api/v1alpha1/expansion_generated.go | 43 + .../listers/api/v1alpha1/helmclusteraddon.go | 70 + .../api/v1alpha1/helmclusteraddonchart.go | 70 + .../api/v1alpha1/helmclusterrepository.go | 70 + api/doc.go | 17 + api/go.mod | 71 + api/go.sum | 171 + api/scripts/boilerplate.go.txt | 15 + api/scripts/update-codegen.sh | 96 + api/v1alpha1/doc.go | 22 + api/v1alpha1/helm_cluster_addon.go | 95 + api/v1alpha1/helm_cluster_addon_chart.go | 73 + api/v1alpha1/helm_cluster_addon_repository.go | 86 + api/v1alpha1/register.go | 71 + api/v1alpha1/zz_generated.deepcopy.go | 391 ++ crds/helmclusteraddoncharts.yaml | 122 + crds/helmclusteraddons.yaml | 161 + crds/helmclusterrepositories.yaml | 145 + images/operator-helm-artifact/.gitignore | 30 + images/operator-helm-artifact/go.mod | 36 + images/operator-helm-artifact/go.sum | 93 + .../pkg/api/openapi/zz_generated.openapi.go | 3218 +++++++++++++++++ 49 files changed, 6959 insertions(+) create mode 100644 api/Taskfile.dist.yaml create mode 100644 api/client/generated/clientset/versioned/clientset.go create mode 100644 api/client/generated/clientset/versioned/fake/clientset_generated.go create mode 100644 api/client/generated/clientset/versioned/fake/doc.go create mode 100644 api/client/generated/clientset/versioned/fake/register.go create mode 100644 api/client/generated/clientset/versioned/scheme/doc.go create mode 100644 api/client/generated/clientset/versioned/scheme/register.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go create mode 100644 api/client/generated/informers/externalversions/api/interface.go create mode 100644 api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go create mode 100644 api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go create mode 100644 api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go create mode 100644 api/client/generated/informers/externalversions/api/v1alpha1/interface.go create mode 100644 api/client/generated/informers/externalversions/factory.go create mode 100644 api/client/generated/informers/externalversions/generic.go create mode 100644 api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go create mode 100644 api/client/generated/listers/api/v1alpha1/expansion_generated.go create mode 100644 api/client/generated/listers/api/v1alpha1/helmclusteraddon.go create mode 100644 api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go create mode 100644 api/client/generated/listers/api/v1alpha1/helmclusterrepository.go create mode 100644 api/doc.go create mode 100644 api/go.mod create mode 100644 api/go.sum create mode 100644 api/scripts/boilerplate.go.txt create mode 100755 api/scripts/update-codegen.sh create mode 100644 api/v1alpha1/doc.go create mode 100644 api/v1alpha1/helm_cluster_addon.go create mode 100644 api/v1alpha1/helm_cluster_addon_chart.go create mode 100644 api/v1alpha1/helm_cluster_addon_repository.go create mode 100644 api/v1alpha1/register.go create mode 100644 api/v1alpha1/zz_generated.deepcopy.go create mode 100644 crds/helmclusteraddoncharts.yaml create mode 100644 crds/helmclusteraddons.yaml create mode 100644 crds/helmclusterrepositories.yaml create mode 100644 images/operator-helm-artifact/.gitignore create mode 100644 images/operator-helm-artifact/go.mod create mode 100644 images/operator-helm-artifact/go.sum create mode 100644 images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go diff --git a/.gitignore b/.gitignore index c2e7f2f..0798ce6 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,8 @@ __pycache__/ # opencode **/.opencode/ + +# Go +go.work +go.work.sum + diff --git a/api/Taskfile.dist.yaml b/api/Taskfile.dist.yaml new file mode 100644 index 0000000..11ed1c9 --- /dev/null +++ b/api/Taskfile.dist.yaml @@ -0,0 +1,41 @@ +version: "3" + +silent: false + +tasks: + generate: + desc: "Regenerate all" + cmds: + - ./scripts/update-codegen.sh all + - task: format:yaml + + generate:v1alpha1: + desc: "Regenerate code for core components." + cmd: ./scripts/update-codegen.sh v1alpha1 + + ci:generate: + desc: "Run generations and check git diff to ensure all files are committed" + cmds: + - task: generate + - task: _ci:verify-gen + + generate:crds: + desc: "Regenerate crds" + cmds: + - ./scripts/update-codegen.sh crds + # - task: format:yaml + + format:yaml: + desc: "Format non-templated YAML files, e.g. CRDs" + cmds: + - | + cd ../ && docker run --rm \ + -v ./:/tmp/virt ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/virt ; prettier -w \"crds/*.yaml\"" + + _ci:verify-gen: + desc: "Check generated files are up-to-date." + internal: true + cmds: + - | + git diff --exit-code || (echo "Please run task gen:api and commit changes" && exit 1) diff --git a/api/client/generated/clientset/versioned/clientset.go b/api/client/generated/clientset/versioned/clientset.go new file mode 100644 index 0000000..93bd3a5 --- /dev/null +++ b/api/client/generated/clientset/versioned/clientset.go @@ -0,0 +1,120 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package versioned + +import ( + fmt "fmt" + http "net/http" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + discovery "k8s.io/client-go/discovery" + rest "k8s.io/client-go/rest" + flowcontrol "k8s.io/client-go/util/flowcontrol" +) + +type Interface interface { + Discovery() discovery.DiscoveryInterface + HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface +} + +// Clientset contains the clients for groups. +type Clientset struct { + *discovery.DiscoveryClient + helmV1alpha1 *helmv1alpha1.HelmV1alpha1Client +} + +// HelmV1alpha1 retrieves the HelmV1alpha1Client +func (c *Clientset) HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface { + return c.helmV1alpha1 +} + +// Discovery retrieves the DiscoveryClient +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + if c == nil { + return nil + } + return c.DiscoveryClient +} + +// NewForConfig creates a new Clientset for the given config. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfig will generate a rate-limiter in configShallowCopy. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*Clientset, error) { + configShallowCopy := *c + + if configShallowCopy.UserAgent == "" { + configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent() + } + + // share the transport between all clients + httpClient, err := rest.HTTPClientFor(&configShallowCopy) + if err != nil { + return nil, err + } + + return NewForConfigAndClient(&configShallowCopy, httpClient) +} + +// NewForConfigAndClient creates a new Clientset for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +// If config's RateLimiter is not set and QPS and Burst are acceptable, +// NewForConfigAndClient will generate a rate-limiter in configShallowCopy. +func NewForConfigAndClient(c *rest.Config, httpClient *http.Client) (*Clientset, error) { + configShallowCopy := *c + if configShallowCopy.RateLimiter == nil && configShallowCopy.QPS > 0 { + if configShallowCopy.Burst <= 0 { + return nil, fmt.Errorf("burst is required to be greater than 0 when RateLimiter is not set and QPS is set to greater than 0") + } + configShallowCopy.RateLimiter = flowcontrol.NewTokenBucketRateLimiter(configShallowCopy.QPS, configShallowCopy.Burst) + } + + var cs Clientset + var err error + cs.helmV1alpha1, err = helmv1alpha1.NewForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + + cs.DiscoveryClient, err = discovery.NewDiscoveryClientForConfigAndClient(&configShallowCopy, httpClient) + if err != nil { + return nil, err + } + return &cs, nil +} + +// NewForConfigOrDie creates a new Clientset for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *Clientset { + cs, err := NewForConfig(c) + if err != nil { + panic(err) + } + return cs +} + +// New creates a new Clientset for the given RESTClient. +func New(c rest.Interface) *Clientset { + var cs Clientset + cs.helmV1alpha1 = helmv1alpha1.New(c) + + cs.DiscoveryClient = discovery.NewDiscoveryClient(c) + return &cs +} diff --git a/api/client/generated/clientset/versioned/fake/clientset_generated.go b/api/client/generated/clientset/versioned/fake/clientset_generated.go new file mode 100644 index 0000000..947f1d9 --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/clientset_generated.go @@ -0,0 +1,105 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + clientset "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + helmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + fakehelmv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/discovery" + fakediscovery "k8s.io/client-go/discovery/fake" + "k8s.io/client-go/testing" +) + +// NewSimpleClientset returns a clientset that will respond with the provided objects. +// It's backed by a very simple object tracker that processes creates, updates and deletions as-is, +// without applying any field management, validations and/or defaults. It shouldn't be considered a replacement +// for a real clientset and is mostly useful in simple unit tests. +// +// Deprecated: NewClientset replaces this with support for field management, which significantly improves +// server side apply testing. NewClientset is only available when apply configurations are generated (e.g. +// via --with-applyconfig). +func NewSimpleClientset(objects ...runtime.Object) *Clientset { + o := testing.NewObjectTracker(scheme, codecs.UniversalDecoder()) + for _, obj := range objects { + if err := o.Add(obj); err != nil { + panic(err) + } + } + + cs := &Clientset{tracker: o} + cs.discovery = &fakediscovery.FakeDiscovery{Fake: &cs.Fake} + cs.AddReactor("*", "*", testing.ObjectReaction(o)) + cs.AddWatchReactor("*", func(action testing.Action) (handled bool, ret watch.Interface, err error) { + var opts metav1.ListOptions + if watchAction, ok := action.(testing.WatchActionImpl); ok { + opts = watchAction.ListOptions + } + gvr := action.GetResource() + ns := action.GetNamespace() + watch, err := o.Watch(gvr, ns, opts) + if err != nil { + return false, nil, err + } + return true, watch, nil + }) + + return cs +} + +// Clientset implements clientset.Interface. Meant to be embedded into a +// struct to get a default implementation. This makes faking out just the method +// you want to test easier. +type Clientset struct { + testing.Fake + discovery *fakediscovery.FakeDiscovery + tracker testing.ObjectTracker +} + +func (c *Clientset) Discovery() discovery.DiscoveryInterface { + return c.discovery +} + +func (c *Clientset) Tracker() testing.ObjectTracker { + return c.tracker +} + +// IsWatchListSemanticsSupported informs the reflector that this client +// doesn't support WatchList semantics. +// +// This is a synthetic method whose sole purpose is to satisfy the optional +// interface check performed by the reflector. +// Returning true signals that WatchList can NOT be used. +// No additional logic is implemented here. +func (c *Clientset) IsWatchListSemanticsUnSupported() bool { + return true +} + +var ( + _ clientset.Interface = &Clientset{} + _ testing.FakeClient = &Clientset{} +) + +// HelmV1alpha1 retrieves the HelmV1alpha1Client +func (c *Clientset) HelmV1alpha1() helmv1alpha1.HelmV1alpha1Interface { + return &fakehelmv1alpha1.FakeHelmV1alpha1{Fake: &c.Fake} +} diff --git a/api/client/generated/clientset/versioned/fake/doc.go b/api/client/generated/clientset/versioned/fake/doc.go new file mode 100644 index 0000000..06b4977 --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/doc.go @@ -0,0 +1,20 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated fake clientset. +package fake diff --git a/api/client/generated/clientset/versioned/fake/register.go b/api/client/generated/clientset/versioned/fake/register.go new file mode 100644 index 0000000..0b233ce --- /dev/null +++ b/api/client/generated/clientset/versioned/fake/register.go @@ -0,0 +1,56 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var scheme = runtime.NewScheme() +var codecs = serializer.NewCodecFactory(scheme) + +var localSchemeBuilder = runtime.SchemeBuilder{ + helmv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(scheme)) +} diff --git a/api/client/generated/clientset/versioned/scheme/doc.go b/api/client/generated/clientset/versioned/scheme/doc.go new file mode 100644 index 0000000..14d115f --- /dev/null +++ b/api/client/generated/clientset/versioned/scheme/doc.go @@ -0,0 +1,20 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package contains the scheme of the automatically generated clientset. +package scheme diff --git a/api/client/generated/clientset/versioned/scheme/register.go b/api/client/generated/clientset/versioned/scheme/register.go new file mode 100644 index 0000000..a1c34af --- /dev/null +++ b/api/client/generated/clientset/versioned/scheme/register.go @@ -0,0 +1,56 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package scheme + +import ( + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + serializer "k8s.io/apimachinery/pkg/runtime/serializer" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" +) + +var Scheme = runtime.NewScheme() +var Codecs = serializer.NewCodecFactory(Scheme) +var ParameterCodec = runtime.NewParameterCodec(Scheme) +var localSchemeBuilder = runtime.SchemeBuilder{ + helmv1alpha1.AddToScheme, +} + +// AddToScheme adds all types of this clientset into the given scheme. This allows composition +// of clientsets, like in: +// +// import ( +// "k8s.io/client-go/kubernetes" +// clientsetscheme "k8s.io/client-go/kubernetes/scheme" +// aggregatorclientsetscheme "k8s.io/kube-aggregator/pkg/client/clientset_generated/clientset/scheme" +// ) +// +// kclientset, _ := kubernetes.NewForConfig(c) +// _ = aggregatorclientsetscheme.AddToScheme(clientsetscheme.Scheme) +// +// After this, RawExtensions in Kubernetes types will serialize kube-aggregator types +// correctly. +var AddToScheme = localSchemeBuilder.AddToScheme + +func init() { + v1.AddToGroupVersion(Scheme, schema.GroupVersion{Version: "v1"}) + utilruntime.Must(AddToScheme(Scheme)) +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go new file mode 100644 index 0000000..2db4d10 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go @@ -0,0 +1,111 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + http "net/http" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + rest "k8s.io/client-go/rest" +) + +type HelmV1alpha1Interface interface { + RESTClient() rest.Interface + HelmClusterAddonsGetter + HelmClusterAddonChartsGetter + HelmClusterRepositoriesGetter +} + +// HelmV1alpha1Client is used to interact with features provided by the helm.deckhouse.io group. +type HelmV1alpha1Client struct { + restClient rest.Interface +} + +func (c *HelmV1alpha1Client) HelmClusterAddons(namespace string) HelmClusterAddonInterface { + return newHelmClusterAddons(c, namespace) +} + +func (c *HelmV1alpha1Client) HelmClusterAddonCharts(namespace string) HelmClusterAddonChartInterface { + return newHelmClusterAddonCharts(c, namespace) +} + +func (c *HelmV1alpha1Client) HelmClusterRepositories(namespace string) HelmClusterRepositoryInterface { + return newHelmClusterRepositories(c, namespace) +} + +// NewForConfig creates a new HelmV1alpha1Client for the given config. +// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient), +// where httpClient was generated with rest.HTTPClientFor(c). +func NewForConfig(c *rest.Config) (*HelmV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + httpClient, err := rest.HTTPClientFor(&config) + if err != nil { + return nil, err + } + return NewForConfigAndClient(&config, httpClient) +} + +// NewForConfigAndClient creates a new HelmV1alpha1Client for the given config and http client. +// Note the http client provided takes precedence over the configured transport values. +func NewForConfigAndClient(c *rest.Config, h *http.Client) (*HelmV1alpha1Client, error) { + config := *c + setConfigDefaults(&config) + client, err := rest.RESTClientForConfigAndClient(&config, h) + if err != nil { + return nil, err + } + return &HelmV1alpha1Client{client}, nil +} + +// NewForConfigOrDie creates a new HelmV1alpha1Client for the given config and +// panics if there is an error in the config. +func NewForConfigOrDie(c *rest.Config) *HelmV1alpha1Client { + client, err := NewForConfig(c) + if err != nil { + panic(err) + } + return client +} + +// New creates a new HelmV1alpha1Client for the given RESTClient. +func New(c rest.Interface) *HelmV1alpha1Client { + return &HelmV1alpha1Client{c} +} + +func setConfigDefaults(config *rest.Config) { + gv := apiv1alpha1.SchemeGroupVersion + config.GroupVersion = &gv + config.APIPath = "/apis" + config.NegotiatedSerializer = rest.CodecFactoryForGeneratedClient(scheme.Scheme, scheme.Codecs).WithoutConversion() + + if config.UserAgent == "" { + config.UserAgent = rest.DefaultKubernetesUserAgent() + } +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *HelmV1alpha1Client) RESTClient() rest.Interface { + if c == nil { + return nil + } + return c.restClient +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go new file mode 100644 index 0000000..51e5450 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/doc.go @@ -0,0 +1,20 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// This package has the automatically generated typed clients. +package v1alpha1 diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go new file mode 100644 index 0000000..ea82301 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/doc.go @@ -0,0 +1,20 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +// Package fake has the automatically generated clients. +package fake diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go new file mode 100644 index 0000000..892587d --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go @@ -0,0 +1,48 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + v1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + rest "k8s.io/client-go/rest" + testing "k8s.io/client-go/testing" +) + +type FakeHelmV1alpha1 struct { + *testing.Fake +} + +func (c *FakeHelmV1alpha1) HelmClusterAddons(namespace string) v1alpha1.HelmClusterAddonInterface { + return newFakeHelmClusterAddons(c, namespace) +} + +func (c *FakeHelmV1alpha1) HelmClusterAddonCharts(namespace string) v1alpha1.HelmClusterAddonChartInterface { + return newFakeHelmClusterAddonCharts(c, namespace) +} + +func (c *FakeHelmV1alpha1) HelmClusterRepositories(namespace string) v1alpha1.HelmClusterRepositoryInterface { + return newFakeHelmClusterRepositories(c, namespace) +} + +// RESTClient returns a RESTClient that is used to communicate +// with API server by this client implementation. +func (c *FakeHelmV1alpha1) RESTClient() rest.Interface { + var ret *rest.RESTClient + return ret +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go new file mode 100644 index 0000000..3e676f6 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go @@ -0,0 +1,52 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddons implements HelmClusterAddonInterface +type fakeHelmClusterAddons struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddon, *v1alpha1.HelmClusterAddonList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddons(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterAddonInterface { + return &fakeHelmClusterAddons{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddon, *v1alpha1.HelmClusterAddonList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddons"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddon"), + func() *v1alpha1.HelmClusterAddon { return &v1alpha1.HelmClusterAddon{} }, + func() *v1alpha1.HelmClusterAddonList { return &v1alpha1.HelmClusterAddonList{} }, + func(dst, src *v1alpha1.HelmClusterAddonList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonList) []*v1alpha1.HelmClusterAddon { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonList, items []*v1alpha1.HelmClusterAddon) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go new file mode 100644 index 0000000..7660143 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go @@ -0,0 +1,52 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddonCharts implements HelmClusterAddonChartInterface +type fakeHelmClusterAddonCharts struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddonChart, *v1alpha1.HelmClusterAddonChartList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddonCharts(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterAddonChartInterface { + return &fakeHelmClusterAddonCharts{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddonChart, *v1alpha1.HelmClusterAddonChartList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddonChart"), + func() *v1alpha1.HelmClusterAddonChart { return &v1alpha1.HelmClusterAddonChart{} }, + func() *v1alpha1.HelmClusterAddonChartList { return &v1alpha1.HelmClusterAddonChartList{} }, + func(dst, src *v1alpha1.HelmClusterAddonChartList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonChartList) []*v1alpha1.HelmClusterAddonChart { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonChartList, items []*v1alpha1.HelmClusterAddonChart) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go new file mode 100644 index 0000000..a0935c7 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go @@ -0,0 +1,52 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterRepositories implements HelmClusterRepositoryInterface +type fakeHelmClusterRepositories struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterRepository, *v1alpha1.HelmClusterRepositoryList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterRepositories(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterRepositoryInterface { + return &fakeHelmClusterRepositories{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterRepository, *v1alpha1.HelmClusterRepositoryList]( + fake.Fake, + namespace, + v1alpha1.SchemeGroupVersion.WithResource("helmclusterrepositories"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterRepository"), + func() *v1alpha1.HelmClusterRepository { return &v1alpha1.HelmClusterRepository{} }, + func() *v1alpha1.HelmClusterRepositoryList { return &v1alpha1.HelmClusterRepositoryList{} }, + func(dst, src *v1alpha1.HelmClusterRepositoryList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterRepositoryList) []*v1alpha1.HelmClusterRepository { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterRepositoryList, items []*v1alpha1.HelmClusterRepository) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go new file mode 100644 index 0000000..0a5f07e --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go @@ -0,0 +1,25 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +type HelmClusterAddonExpansion interface{} + +type HelmClusterAddonChartExpansion interface{} + +type HelmClusterRepositoryExpansion interface{} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..dfc0dc8 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonsGetter has a method to return a HelmClusterAddonInterface. +// A group's client should implement this interface. +type HelmClusterAddonsGetter interface { + HelmClusterAddons(namespace string) HelmClusterAddonInterface +} + +// HelmClusterAddonInterface has methods to work with HelmClusterAddon resources. +type HelmClusterAddonInterface interface { + Create(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddon, error) + Update(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddon, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddon *apiv1alpha1.HelmClusterAddon, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddon, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddon, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddon, err error) + HelmClusterAddonExpansion +} + +// helmClusterAddons implements HelmClusterAddonInterface +type helmClusterAddons struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddon, *apiv1alpha1.HelmClusterAddonList] +} + +// newHelmClusterAddons returns a HelmClusterAddons +func newHelmClusterAddons(c *HelmV1alpha1Client, namespace string) *helmClusterAddons { + return &helmClusterAddons{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddon, *apiv1alpha1.HelmClusterAddonList]( + "helmclusteraddons", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.HelmClusterAddon { return &apiv1alpha1.HelmClusterAddon{} }, + func() *apiv1alpha1.HelmClusterAddonList { return &apiv1alpha1.HelmClusterAddonList{} }, + ), + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..67de76c --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonChartsGetter has a method to return a HelmClusterAddonChartInterface. +// A group's client should implement this interface. +type HelmClusterAddonChartsGetter interface { + HelmClusterAddonCharts(namespace string) HelmClusterAddonChartInterface +} + +// HelmClusterAddonChartInterface has methods to work with HelmClusterAddonChart resources. +type HelmClusterAddonChartInterface interface { + Create(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + Update(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddonChart *apiv1alpha1.HelmClusterAddonChart, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddonChart, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonChartList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddonChart, err error) + HelmClusterAddonChartExpansion +} + +// helmClusterAddonCharts implements HelmClusterAddonChartInterface +type helmClusterAddonCharts struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddonChart, *apiv1alpha1.HelmClusterAddonChartList] +} + +// newHelmClusterAddonCharts returns a HelmClusterAddonCharts +func newHelmClusterAddonCharts(c *HelmV1alpha1Client, namespace string) *helmClusterAddonCharts { + return &helmClusterAddonCharts{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddonChart, *apiv1alpha1.HelmClusterAddonChartList]( + "helmclusteraddoncharts", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.HelmClusterAddonChart { return &apiv1alpha1.HelmClusterAddonChart{} }, + func() *apiv1alpha1.HelmClusterAddonChartList { return &apiv1alpha1.HelmClusterAddonChartList{} }, + ), + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go new file mode 100644 index 0000000..d0a84e5 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterRepositoriesGetter has a method to return a HelmClusterRepositoryInterface. +// A group's client should implement this interface. +type HelmClusterRepositoriesGetter interface { + HelmClusterRepositories(namespace string) HelmClusterRepositoryInterface +} + +// HelmClusterRepositoryInterface has methods to work with HelmClusterRepository resources. +type HelmClusterRepositoryInterface interface { + Create(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterRepository, error) + Update(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterRepository, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterRepository, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterRepository, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterRepositoryList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterRepository, err error) + HelmClusterRepositoryExpansion +} + +// helmClusterRepositories implements HelmClusterRepositoryInterface +type helmClusterRepositories struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterRepository, *apiv1alpha1.HelmClusterRepositoryList] +} + +// newHelmClusterRepositories returns a HelmClusterRepositories +func newHelmClusterRepositories(c *HelmV1alpha1Client, namespace string) *helmClusterRepositories { + return &helmClusterRepositories{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterRepository, *apiv1alpha1.HelmClusterRepositoryList]( + "helmclusterrepositories", + c.RESTClient(), + scheme.ParameterCodec, + namespace, + func() *apiv1alpha1.HelmClusterRepository { return &apiv1alpha1.HelmClusterRepository{} }, + func() *apiv1alpha1.HelmClusterRepositoryList { return &apiv1alpha1.HelmClusterRepositoryList{} }, + ), + } +} diff --git a/api/client/generated/informers/externalversions/api/interface.go b/api/client/generated/informers/externalversions/api/interface.go new file mode 100644 index 0000000..2977c43 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/interface.go @@ -0,0 +1,46 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package api + +import ( + v1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/api/v1alpha1" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to each of this group's versions. +type Interface interface { + // V1alpha1 provides access to shared informers for resources in V1alpha1. + V1alpha1() v1alpha1.Interface +} + +type group struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &group{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// V1alpha1 returns a new v1alpha1.Interface. +func (g *group) V1alpha1() v1alpha1.Interface { + return v1alpha1.New(g.factory, g.namespace, g.tweakListOptions) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..40857e6 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,102 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonInformer provides access to a shared informer and lister for +// HelmClusterAddons. +type HelmClusterAddonInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterAddonLister +} + +type helmClusterAddonInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddons(namespace).Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterAddon{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterAddonInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterAddonInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddon{}, f.defaultInformer) +} + +func (f *helmClusterAddonInformer) Lister() apiv1alpha1.HelmClusterAddonLister { + return apiv1alpha1.NewHelmClusterAddonLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..9d82d80 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,102 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonChartInformer provides access to a shared informer and lister for +// HelmClusterAddonCharts. +type HelmClusterAddonChartInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterAddonChartLister +} + +type helmClusterAddonChartInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterAddonChartInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonChartInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterAddonChartInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterAddonChart{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterAddonChartInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonChartInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterAddonChartInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddonChart{}, f.defaultInformer) +} + +func (f *helmClusterAddonChartInformer) Lister() apiv1alpha1.HelmClusterAddonChartLister { + return apiv1alpha1.NewHelmClusterAddonChartLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go new file mode 100644 index 0000000..4a70c37 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go @@ -0,0 +1,102 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/listers/api/v1alpha1" + operatorhelmapiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + watch "k8s.io/apimachinery/pkg/watch" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterRepositoryInformer provides access to a shared informer and lister for +// HelmClusterRepositories. +type HelmClusterRepositoryInformer interface { + Informer() cache.SharedIndexInformer + Lister() apiv1alpha1.HelmClusterRepositoryLister +} + +type helmClusterRepositoryInformer struct { + factory internalinterfaces.SharedInformerFactory + tweakListOptions internalinterfaces.TweakListOptionsFunc + namespace string +} + +// NewHelmClusterRepositoryInformer constructs a new informer for HelmClusterRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewHelmClusterRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterRepositoryInformer(client, namespace, resyncPeriod, indexers, nil) +} + +// NewFilteredHelmClusterRepositoryInformer constructs a new informer for HelmClusterRepository type. +// Always prefer using an informer factory to get a shared informer instead of getting an independent +// one. This reduces memory footprint and number of connections to the server. +func NewFilteredHelmClusterRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { + return cache.NewSharedIndexInformer( + cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ + ListFunc: func(options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterRepositories(namespace).List(context.Background(), options) + }, + WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterRepositories(namespace).Watch(context.Background(), options) + }, + ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterRepositories(namespace).List(ctx, options) + }, + WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { + if tweakListOptions != nil { + tweakListOptions(&options) + } + return client.HelmV1alpha1().HelmClusterRepositories(namespace).Watch(ctx, options) + }, + }, client), + &operatorhelmapiv1alpha1.HelmClusterRepository{}, + resyncPeriod, + indexers, + ) +} + +func (f *helmClusterRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterRepositoryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +} + +func (f *helmClusterRepositoryInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterRepository{}, f.defaultInformer) +} + +func (f *helmClusterRepositoryInformer) Lister() apiv1alpha1.HelmClusterRepositoryLister { + return apiv1alpha1.NewHelmClusterRepositoryLister(f.Informer().GetIndexer()) +} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/interface.go b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go new file mode 100644 index 0000000..15233c7 --- /dev/null +++ b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go @@ -0,0 +1,59 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" +) + +// Interface provides access to all the informers in this group version. +type Interface interface { + // HelmClusterAddons returns a HelmClusterAddonInformer. + HelmClusterAddons() HelmClusterAddonInformer + // HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. + HelmClusterAddonCharts() HelmClusterAddonChartInformer + // HelmClusterRepositories returns a HelmClusterRepositoryInformer. + HelmClusterRepositories() HelmClusterRepositoryInformer +} + +type version struct { + factory internalinterfaces.SharedInformerFactory + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc +} + +// New returns a new Interface. +func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) Interface { + return &version{factory: f, namespace: namespace, tweakListOptions: tweakListOptions} +} + +// HelmClusterAddons returns a HelmClusterAddonInformer. +func (v *version) HelmClusterAddons() HelmClusterAddonInformer { + return &helmClusterAddonInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. +func (v *version) HelmClusterAddonCharts() HelmClusterAddonChartInformer { + return &helmClusterAddonChartInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} + +// HelmClusterRepositories returns a HelmClusterRepositoryInformer. +func (v *version) HelmClusterRepositories() HelmClusterRepositoryInformer { + return &helmClusterRepositoryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +} diff --git a/api/client/generated/informers/externalversions/factory.go b/api/client/generated/informers/externalversions/factory.go new file mode 100644 index 0000000..df93e62 --- /dev/null +++ b/api/client/generated/informers/externalversions/factory.go @@ -0,0 +1,263 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + reflect "reflect" + sync "sync" + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + api "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/api" + internalinterfaces "github.com/deckhouse/operator-helm/api/client/generated/informers/externalversions/internalinterfaces" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// SharedInformerOption defines the functional option type for SharedInformerFactory. +type SharedInformerOption func(*sharedInformerFactory) *sharedInformerFactory + +type sharedInformerFactory struct { + client versioned.Interface + namespace string + tweakListOptions internalinterfaces.TweakListOptionsFunc + lock sync.Mutex + defaultResync time.Duration + customResync map[reflect.Type]time.Duration + transform cache.TransformFunc + + informers map[reflect.Type]cache.SharedIndexInformer + // startedInformers is used for tracking which informers have been started. + // This allows Start() to be called multiple times safely. + startedInformers map[reflect.Type]bool + // wg tracks how many goroutines were started. + wg sync.WaitGroup + // shuttingDown is true when Shutdown has been called. It may still be running + // because it needs to wait for goroutines. + shuttingDown bool +} + +// WithCustomResyncConfig sets a custom resync period for the specified informer types. +func WithCustomResyncConfig(resyncConfig map[v1.Object]time.Duration) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + for k, v := range resyncConfig { + factory.customResync[reflect.TypeOf(k)] = v + } + return factory + } +} + +// WithTweakListOptions sets a custom filter on all listers of the configured SharedInformerFactory. +func WithTweakListOptions(tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.tweakListOptions = tweakListOptions + return factory + } +} + +// WithNamespace limits the SharedInformerFactory to the specified namespace. +func WithNamespace(namespace string) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.namespace = namespace + return factory + } +} + +// WithTransform sets a transform on all informers. +func WithTransform(transform cache.TransformFunc) SharedInformerOption { + return func(factory *sharedInformerFactory) *sharedInformerFactory { + factory.transform = transform + return factory + } +} + +// NewSharedInformerFactory constructs a new instance of sharedInformerFactory for all namespaces. +func NewSharedInformerFactory(client versioned.Interface, defaultResync time.Duration) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync) +} + +// NewFilteredSharedInformerFactory constructs a new instance of sharedInformerFactory. +// Listers obtained via this SharedInformerFactory will be subject to the same filters +// as specified here. +// +// Deprecated: Please use NewSharedInformerFactoryWithOptions instead +func NewFilteredSharedInformerFactory(client versioned.Interface, defaultResync time.Duration, namespace string, tweakListOptions internalinterfaces.TweakListOptionsFunc) SharedInformerFactory { + return NewSharedInformerFactoryWithOptions(client, defaultResync, WithNamespace(namespace), WithTweakListOptions(tweakListOptions)) +} + +// NewSharedInformerFactoryWithOptions constructs a new instance of a SharedInformerFactory with additional options. +func NewSharedInformerFactoryWithOptions(client versioned.Interface, defaultResync time.Duration, options ...SharedInformerOption) SharedInformerFactory { + factory := &sharedInformerFactory{ + client: client, + namespace: v1.NamespaceAll, + defaultResync: defaultResync, + informers: make(map[reflect.Type]cache.SharedIndexInformer), + startedInformers: make(map[reflect.Type]bool), + customResync: make(map[reflect.Type]time.Duration), + } + + // Apply all options + for _, opt := range options { + factory = opt(factory) + } + + return factory +} + +func (f *sharedInformerFactory) Start(stopCh <-chan struct{}) { + f.lock.Lock() + defer f.lock.Unlock() + + if f.shuttingDown { + return + } + + for informerType, informer := range f.informers { + if !f.startedInformers[informerType] { + f.wg.Add(1) + // We need a new variable in each loop iteration, + // otherwise the goroutine would use the loop variable + // and that keeps changing. + informer := informer + go func() { + defer f.wg.Done() + informer.Run(stopCh) + }() + f.startedInformers[informerType] = true + } + } +} + +func (f *sharedInformerFactory) Shutdown() { + f.lock.Lock() + f.shuttingDown = true + f.lock.Unlock() + + // Will return immediately if there is nothing to wait for. + f.wg.Wait() +} + +func (f *sharedInformerFactory) WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool { + informers := func() map[reflect.Type]cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informers := map[reflect.Type]cache.SharedIndexInformer{} + for informerType, informer := range f.informers { + if f.startedInformers[informerType] { + informers[informerType] = informer + } + } + return informers + }() + + res := map[reflect.Type]bool{} + for informType, informer := range informers { + res[informType] = cache.WaitForCacheSync(stopCh, informer.HasSynced) + } + return res +} + +// InformerFor returns the SharedIndexInformer for obj using an internal +// client. +func (f *sharedInformerFactory) InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer { + f.lock.Lock() + defer f.lock.Unlock() + + informerType := reflect.TypeOf(obj) + informer, exists := f.informers[informerType] + if exists { + return informer + } + + resyncPeriod, exists := f.customResync[informerType] + if !exists { + resyncPeriod = f.defaultResync + } + + informer = newFunc(f.client, resyncPeriod) + informer.SetTransform(f.transform) + f.informers[informerType] = informer + + return informer +} + +// SharedInformerFactory provides shared informers for resources in all known +// API group versions. +// +// It is typically used like this: +// +// ctx, cancel := context.WithCancel(context.Background()) +// defer cancel() +// factory := NewSharedInformerFactory(client, resyncPeriod) +// defer factory.WaitForStop() // Returns immediately if nothing was started. +// genericInformer := factory.ForResource(resource) +// typedInformer := factory.SomeAPIGroup().V1().SomeType() +// factory.Start(ctx.Done()) // Start processing these informers. +// synced := factory.WaitForCacheSync(ctx.Done()) +// for v, ok := range synced { +// if !ok { +// fmt.Fprintf(os.Stderr, "caches failed to sync: %v", v) +// return +// } +// } +// +// // Creating informers can also be created after Start, but then +// // Start must be called again: +// anotherGenericInformer := factory.ForResource(resource) +// factory.Start(ctx.Done()) +type SharedInformerFactory interface { + internalinterfaces.SharedInformerFactory + + // Start initializes all requested informers. They are handled in goroutines + // which run until the stop channel gets closed. + // Warning: Start does not block. When run in a go-routine, it will race with a later WaitForCacheSync. + Start(stopCh <-chan struct{}) + + // Shutdown marks a factory as shutting down. At that point no new + // informers can be started anymore and Start will return without + // doing anything. + // + // In addition, Shutdown blocks until all goroutines have terminated. For that + // to happen, the close channel(s) that they were started with must be closed, + // either before Shutdown gets called or while it is waiting. + // + // Shutdown may be called multiple times, even concurrently. All such calls will + // block until all goroutines have terminated. + Shutdown() + + // WaitForCacheSync blocks until all started informers' caches were synced + // or the stop channel gets closed. + WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool + + // ForResource gives generic access to a shared informer of the matching type. + ForResource(resource schema.GroupVersionResource) (GenericInformer, error) + + // InformerFor returns the SharedIndexInformer for obj using an internal + // client. + InformerFor(obj runtime.Object, newFunc internalinterfaces.NewInformerFunc) cache.SharedIndexInformer + + Helm() api.Interface +} + +func (f *sharedInformerFactory) Helm() api.Interface { + return api.New(f, f.namespace, f.tweakListOptions) +} diff --git a/api/client/generated/informers/externalversions/generic.go b/api/client/generated/informers/externalversions/generic.go new file mode 100644 index 0000000..d355a0e --- /dev/null +++ b/api/client/generated/informers/externalversions/generic.go @@ -0,0 +1,66 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package externalversions + +import ( + fmt "fmt" + + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + schema "k8s.io/apimachinery/pkg/runtime/schema" + cache "k8s.io/client-go/tools/cache" +) + +// GenericInformer is type of SharedIndexInformer which will locate and delegate to other +// sharedInformers based on type +type GenericInformer interface { + Informer() cache.SharedIndexInformer + Lister() cache.GenericLister +} + +type genericInformer struct { + informer cache.SharedIndexInformer + resource schema.GroupResource +} + +// Informer returns the SharedIndexInformer. +func (f *genericInformer) Informer() cache.SharedIndexInformer { + return f.informer +} + +// Lister returns the GenericLister. +func (f *genericInformer) Lister() cache.GenericLister { + return cache.NewGenericLister(f.Informer().GetIndexer(), f.resource) +} + +// ForResource gives generic access to a shared informer of the matching type +// TODO extend this to unknown resources with a client pool +func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource) (GenericInformer, error) { + switch resource { + // Group=helm.deckhouse.io, Version=v1alpha1 + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddons"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddons().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddonCharts().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("helmclusterrepositories"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterRepositories().Informer()}, nil + + } + + return nil, fmt.Errorf("no informer found for %v", resource) +} diff --git a/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go b/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go new file mode 100644 index 0000000..77db6c7 --- /dev/null +++ b/api/client/generated/informers/externalversions/internalinterfaces/factory_interfaces.go @@ -0,0 +1,40 @@ +/* +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. +*/ + +// Code generated by informer-gen. DO NOT EDIT. + +package internalinterfaces + +import ( + time "time" + + versioned "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + cache "k8s.io/client-go/tools/cache" +) + +// NewInformerFunc takes versioned.Interface and time.Duration to return a SharedIndexInformer. +type NewInformerFunc func(versioned.Interface, time.Duration) cache.SharedIndexInformer + +// SharedInformerFactory a small interface to allow for adding an informer without an import cycle +type SharedInformerFactory interface { + Start(stopCh <-chan struct{}) + InformerFor(obj runtime.Object, newFunc NewInformerFunc) cache.SharedIndexInformer +} + +// TweakListOptionsFunc is a function that transforms a v1.ListOptions. +type TweakListOptionsFunc func(*v1.ListOptions) diff --git a/api/client/generated/listers/api/v1alpha1/expansion_generated.go b/api/client/generated/listers/api/v1alpha1/expansion_generated.go new file mode 100644 index 0000000..9261d48 --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/expansion_generated.go @@ -0,0 +1,43 @@ +/* +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +// HelmClusterAddonListerExpansion allows custom methods to be added to +// HelmClusterAddonLister. +type HelmClusterAddonListerExpansion interface{} + +// HelmClusterAddonNamespaceListerExpansion allows custom methods to be added to +// HelmClusterAddonNamespaceLister. +type HelmClusterAddonNamespaceListerExpansion interface{} + +// HelmClusterAddonChartListerExpansion allows custom methods to be added to +// HelmClusterAddonChartLister. +type HelmClusterAddonChartListerExpansion interface{} + +// HelmClusterAddonChartNamespaceListerExpansion allows custom methods to be added to +// HelmClusterAddonChartNamespaceLister. +type HelmClusterAddonChartNamespaceListerExpansion interface{} + +// HelmClusterRepositoryListerExpansion allows custom methods to be added to +// HelmClusterRepositoryLister. +type HelmClusterRepositoryListerExpansion interface{} + +// HelmClusterRepositoryNamespaceListerExpansion allows custom methods to be added to +// HelmClusterRepositoryNamespaceLister. +type HelmClusterRepositoryNamespaceListerExpansion interface{} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go new file mode 100644 index 0000000..7752bd6 --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonLister helps list HelmClusterAddons. +// All objects returned here must be treated as read-only. +type HelmClusterAddonLister interface { + // List lists all HelmClusterAddons in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddon, err error) + // HelmClusterAddons returns an object that can list and get HelmClusterAddons. + HelmClusterAddons(namespace string) HelmClusterAddonNamespaceLister + HelmClusterAddonListerExpansion +} + +// helmClusterAddonLister implements the HelmClusterAddonLister interface. +type helmClusterAddonLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddon] +} + +// NewHelmClusterAddonLister returns a new HelmClusterAddonLister. +func NewHelmClusterAddonLister(indexer cache.Indexer) HelmClusterAddonLister { + return &helmClusterAddonLister{listers.New[*apiv1alpha1.HelmClusterAddon](indexer, apiv1alpha1.Resource("helmclusteraddon"))} +} + +// HelmClusterAddons returns an object that can list and get HelmClusterAddons. +func (s *helmClusterAddonLister) HelmClusterAddons(namespace string) HelmClusterAddonNamespaceLister { + return helmClusterAddonNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterAddon](s.ResourceIndexer, namespace)} +} + +// HelmClusterAddonNamespaceLister helps list and get HelmClusterAddons. +// All objects returned here must be treated as read-only. +type HelmClusterAddonNamespaceLister interface { + // List lists all HelmClusterAddons in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddon, err error) + // Get retrieves the HelmClusterAddon from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddon, error) + HelmClusterAddonNamespaceListerExpansion +} + +// helmClusterAddonNamespaceLister implements the HelmClusterAddonNamespaceLister +// interface. +type helmClusterAddonNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddon] +} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go new file mode 100644 index 0000000..6bfc83c --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonChartLister helps list HelmClusterAddonCharts. +// All objects returned here must be treated as read-only. +type HelmClusterAddonChartLister interface { + // List lists all HelmClusterAddonCharts in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonChart, err error) + // HelmClusterAddonCharts returns an object that can list and get HelmClusterAddonCharts. + HelmClusterAddonCharts(namespace string) HelmClusterAddonChartNamespaceLister + HelmClusterAddonChartListerExpansion +} + +// helmClusterAddonChartLister implements the HelmClusterAddonChartLister interface. +type helmClusterAddonChartLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonChart] +} + +// NewHelmClusterAddonChartLister returns a new HelmClusterAddonChartLister. +func NewHelmClusterAddonChartLister(indexer cache.Indexer) HelmClusterAddonChartLister { + return &helmClusterAddonChartLister{listers.New[*apiv1alpha1.HelmClusterAddonChart](indexer, apiv1alpha1.Resource("helmclusteraddonchart"))} +} + +// HelmClusterAddonCharts returns an object that can list and get HelmClusterAddonCharts. +func (s *helmClusterAddonChartLister) HelmClusterAddonCharts(namespace string) HelmClusterAddonChartNamespaceLister { + return helmClusterAddonChartNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterAddonChart](s.ResourceIndexer, namespace)} +} + +// HelmClusterAddonChartNamespaceLister helps list and get HelmClusterAddonCharts. +// All objects returned here must be treated as read-only. +type HelmClusterAddonChartNamespaceLister interface { + // List lists all HelmClusterAddonCharts in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonChart, err error) + // Get retrieves the HelmClusterAddonChart from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddonChart, error) + HelmClusterAddonChartNamespaceListerExpansion +} + +// helmClusterAddonChartNamespaceLister implements the HelmClusterAddonChartNamespaceLister +// interface. +type helmClusterAddonChartNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonChart] +} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go b/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go new file mode 100644 index 0000000..636260c --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go @@ -0,0 +1,70 @@ +/* +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterRepositoryLister helps list HelmClusterRepositories. +// All objects returned here must be treated as read-only. +type HelmClusterRepositoryLister interface { + // List lists all HelmClusterRepositories in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterRepository, err error) + // HelmClusterRepositories returns an object that can list and get HelmClusterRepositories. + HelmClusterRepositories(namespace string) HelmClusterRepositoryNamespaceLister + HelmClusterRepositoryListerExpansion +} + +// helmClusterRepositoryLister implements the HelmClusterRepositoryLister interface. +type helmClusterRepositoryLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterRepository] +} + +// NewHelmClusterRepositoryLister returns a new HelmClusterRepositoryLister. +func NewHelmClusterRepositoryLister(indexer cache.Indexer) HelmClusterRepositoryLister { + return &helmClusterRepositoryLister{listers.New[*apiv1alpha1.HelmClusterRepository](indexer, apiv1alpha1.Resource("helmclusterrepository"))} +} + +// HelmClusterRepositories returns an object that can list and get HelmClusterRepositories. +func (s *helmClusterRepositoryLister) HelmClusterRepositories(namespace string) HelmClusterRepositoryNamespaceLister { + return helmClusterRepositoryNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterRepository](s.ResourceIndexer, namespace)} +} + +// HelmClusterRepositoryNamespaceLister helps list and get HelmClusterRepositories. +// All objects returned here must be treated as read-only. +type HelmClusterRepositoryNamespaceLister interface { + // List lists all HelmClusterRepositories in the indexer for a given namespace. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterRepository, err error) + // Get retrieves the HelmClusterRepository from the indexer for a given namespace and name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterRepository, error) + HelmClusterRepositoryNamespaceListerExpansion +} + +// helmClusterRepositoryNamespaceLister implements the HelmClusterRepositoryNamespaceLister +// interface. +type helmClusterRepositoryNamespaceLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterRepository] +} diff --git a/api/doc.go b/api/doc.go new file mode 100644 index 0000000..8cb1e54 --- /dev/null +++ b/api/doc.go @@ -0,0 +1,17 @@ +/* +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 api diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..0231311 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,71 @@ +module github.com/deckhouse/operator-helm/api + +go 1.25.0 + +tool ( + k8s.io/code-generator + k8s.io/kube-openapi/cmd/openapi-gen + sigs.k8s.io/controller-tools/cmd/controller-gen +) + +require ( + k8s.io/apiextensions-apiserver v0.35.1 + k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.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/gobuffalo/flect v1.0.3 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // 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 + github.com/onsi/gomega v1.38.3 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/mod v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.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/time v0.9.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/code-generator v0.35.1 // indirect + k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-tools v0.17.2 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..6a95e49 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,171 @@ +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +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-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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +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/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/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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/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/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/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/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +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/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/code-generator v0.35.1 h1:yLKR2la7Z9cWT5qmk67ayx8xXLM4RRKQMnC8YPvTWRI= +k8s.io/code-generator v0.35.1/go.mod h1:F2Fhm7aA69tC/VkMXLDokdovltXEF026Tb9yfQXQWKg= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= +k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= +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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg= +sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/api/scripts/boilerplate.go.txt b/api/scripts/boilerplate.go.txt new file mode 100644 index 0000000..cc60635 --- /dev/null +++ b/api/scripts/boilerplate.go.txt @@ -0,0 +1,15 @@ +/* +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. +*/ diff --git a/api/scripts/update-codegen.sh b/api/scripts/update-codegen.sh new file mode 100755 index 0000000..0d60587 --- /dev/null +++ b/api/scripts/update-codegen.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +# Copyright 2024 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. +set -o errexit +set -o nounset +set -o pipefail + +function usage { + cat <', where is the name of a field in a struct, or key in a map 'v:', where is the exact json formatted value of a list item 'i:', where is position of a item in a list 'k:', where is a map of a list item's key fields to their unique values If a key maps to an empty Fields value, the field that key represents is part of the set.\n\nThe exact format is defined in sigs.k8s.io/structured-merge-diff", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GetOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GetOptions is the standard query options to the standard REST get call.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "kind"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupResource specifies a Group and a Resource, but does not force a version. This is useful for identifying concepts during lookup stages without having partially valid types", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "resource"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersion(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersion contains the \"group\" and the \"version\", which uniquely identifies the API.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersion contains the \"group/version\" and \"version\" string of a version. It is made a struct to keep extensibility.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "groupVersion": { + SchemaProps: spec.SchemaProps{ + Description: "groupVersion specifies the API group and version in the form \"group/version\"", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "version specifies the version in the form of \"version\". This is to save the clients the trouble of splitting the GroupVersion.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"groupVersion", "version"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionKind(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version", "kind"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_GroupVersionResource(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "group": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "resource": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"group", "version", "resource"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_InternalEvent(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "InternalEvent makes watch.Event versioned", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "Object": { + SchemaProps: spec.SchemaProps{ + Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Bookmark: the object (instance of a type being watched) where\n only ResourceVersion field is set. On successful restart of watch from a\n bookmark resourceVersion, client is guaranteed to not get repeat event\n nor miss any events.\n * If Type is Error: *api.Status is recommended; other types may make sense\n depending on context.", + }, + }, + }, + Required: []string{"Type", "Object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_LabelSelector(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "A label selector is a label query over a set of resources. The result of matchLabels and matchExpressions are ANDed. An empty label selector matches all objects. A null label selector matches no objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "matchLabels": { + SchemaProps: spec.SchemaProps{ + Description: "matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is \"key\", the operator is \"In\", and the values array contains only \"value\". The requirements are ANDed.", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "matchExpressions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "matchExpressions is a list of label selector requirements. The requirements are ANDed.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.LabelSelectorRequirement{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + }, + Dependencies: []string{ + v1.LabelSelectorRequirement{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "key": { + SchemaProps: spec.SchemaProps{ + Description: "key is the label key that the selector applies to.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "operator": { + SchemaProps: spec.SchemaProps{ + Description: "operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "values": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"key", "operator"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_List(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "List holds a list of objects, which may not be known by the server.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "List of objects", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_pkg_apis_meta_v1_ListMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ListMeta describes metadata that synthetic resources must have, including lists and various status objects. A resource may have only one of {ObjectMeta, ListMeta}.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "selfLink": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "String that identifies the server's internal version of this object that can be used by clients to determine when objects have changed. Value must be treated as opaque by clients and passed unmodified back to the server. Populated by the system. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + Type: []string{"string"}, + Format: "", + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Description: "continue may be set if the user set a limit on the number of items returned, and indicates that the server has more data available. The value is opaque and may be used to issue another request to the endpoint that served this list to retrieve the next set of available objects. Continuing a consistent list may not be possible if the server configuration has changed or more than a few minutes have passed. The resourceVersion field returned when using this continue value will be identical to the value in the first response, unless you have received this token from an error message.", + Type: []string{"string"}, + Format: "", + }, + }, + "remainingItemCount": { + SchemaProps: spec.SchemaProps{ + Description: "remainingItemCount is the number of subsequent items in the list which are not included in this list response. If the list request contained label or field selectors, then the number of remaining items is unknown and the field will be left unset and omitted during serialization. If the list is complete (either because it is not chunking or because this is the last chunk), then there are no more remaining items and this field will be left unset and omitted during serialization. Servers older than v1.15 do not set this field. The intended use of the remainingItemCount is *estimating* the size of a collection. Clients should not rely on the remainingItemCount to be set or to be exact.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ListOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ListOptions is the query options to a standard REST list call.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "labelSelector": { + SchemaProps: spec.SchemaProps{ + Description: "A selector to restrict the list of returned objects by their labels. Defaults to everything.", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldSelector": { + SchemaProps: spec.SchemaProps{ + Description: "A selector to restrict the list of returned objects by their fields. Defaults to everything.", + Type: []string{"string"}, + Format: "", + }, + }, + "watch": { + SchemaProps: spec.SchemaProps{ + Description: "Watch for changes to the described resources and return them as a stream of add, update, and remove notifications. Specify resourceVersion.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "allowWatchBookmarks": { + SchemaProps: spec.SchemaProps{ + Description: "allowWatchBookmarks requests watch events with type \"BOOKMARK\". Servers that do not implement bookmarks may ignore this flag and bookmarks are sent at the server's discretion. Clients should not assume bookmarks are returned at any specific interval, nor may they assume the server will send any BOOKMARK event during a session. If this is not a watch, this field is ignored.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersion sets a constraint on what resource versions a request may be served from. See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersionMatch": { + SchemaProps: spec.SchemaProps{ + Description: "resourceVersionMatch determines how resourceVersion is applied to list calls. It is highly recommended that resourceVersionMatch be set for list calls where resourceVersion is set See https://kubernetes.io/docs/reference/using-api/api-concepts/#resource-versions for details.\n\nDefaults to unset", + Type: []string{"string"}, + Format: "", + }, + }, + "timeoutSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "Timeout for the list/watch call. This limits the duration of the call, regardless of any activity or inactivity.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "limit": { + SchemaProps: spec.SchemaProps{ + Description: "limit is a maximum number of responses to return for a list call. If more items exist, the server will set the `continue` field on the list metadata to a value that can be used with the same initial query to retrieve the next set of results. Setting a limit may return fewer than the requested amount of items (up to zero items) in the event all requested objects are filtered out and clients should only use the presence of the continue field to determine whether more results are available. Servers may choose not to support the limit argument and will return all of the available results. If limit is specified and the continue field is empty, clients may assume that no more results are available. This field is not supported if watch is true.\n\nThe server guarantees that the objects returned when using continue will be identical to issuing a single list call without a limit - that is, no objects created, modified, or deleted after the first request is issued will be included in any subsequent continued requests. This is sometimes referred to as a consistent snapshot, and ensures that a client that is using limit to receive smaller chunks of a very large result can ensure they see all possible objects. If objects are updated during a chunked list the version of the object that was present at the time the first list result was calculated is returned.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "continue": { + SchemaProps: spec.SchemaProps{ + Description: "The continue option should be set when retrieving more results from the server. Since this value is server defined, clients may only use the continue value from a previous query result with identical query parameters (except for the value of continue) and the server may reject a continue value it does not recognize. If the specified continue value is no longer valid whether due to expiration (generally five to fifteen minutes) or a configuration change on the server, the server will respond with a 410 ResourceExpired error together with a continue token. If the client needs a consistent list, it must restart their list without the continue field. Otherwise, the client may send another list request with the token received with the 410 error, the server will respond with a list starting from the next key, but from the latest snapshot, which is inconsistent from the previous list results - objects that are created, modified, or deleted after the first list request will be included in the response, as long as their keys are after the \"next key\".\n\nThis field is not supported when watch is true. Clients may start a watch from the last resourceVersion value returned by the server and not miss any modifications.", + Type: []string{"string"}, + Format: "", + }, + }, + "sendInitialEvents": { + SchemaProps: spec.SchemaProps{ + Description: "`sendInitialEvents=true` may be set together with `watch=true`. In that case, the watch stream will begin with synthetic events to produce the current state of objects in the collection. Once all such events have been sent, a synthetic \"Bookmark\" event will be sent. The bookmark will report the ResourceVersion (RV) corresponding to the set of objects, and be marked with `\"k8s.io/initial-events-end\": \"true\"` annotation. Afterwards, the watch stream will proceed as usual, sending watch events corresponding to changes (subsequent to the RV) to objects watched.\n\nWhen `sendInitialEvents` option is set, we require `resourceVersionMatch` option to also be set. The semantic of the watch request is as following: - `resourceVersionMatch` = NotOlderThan\n is interpreted as \"data at least as new as the provided `resourceVersion`\"\n and the bookmark event is send when the state is synced\n to a `resourceVersion` at least as fresh as the one provided by the ListOptions.\n If `resourceVersion` is unset, this is interpreted as \"consistent read\" and the\n bookmark event is send when the state is synced at least to the moment\n when request started being processed.\n- `resourceVersionMatch` set to any other value or unset\n Invalid error is returned.\n\nDefaults to true if `resourceVersion=\"\"` or `resourceVersion=\"0\"` (for backward compatibility reasons) and to false otherwise.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ManagedFieldsEntry is a workflow-id, a FieldSet and the group version of the resource that the fieldset applies to.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "manager": { + SchemaProps: spec.SchemaProps{ + Description: "Manager is an identifier of the workflow managing these fields.", + Type: []string{"string"}, + Format: "", + }, + }, + "operation": { + SchemaProps: spec.SchemaProps{ + Description: "Operation is the type of operation which lead to this ManagedFieldsEntry being created. The only valid values for this field are 'Apply' and 'Update'.", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the version of this resource that this field set applies to. The format is \"group/version\" just like the top-level APIVersion field. It is necessary to track the version of a field set because it cannot be automatically converted.", + Type: []string{"string"}, + Format: "", + }, + }, + "time": { + SchemaProps: spec.SchemaProps{ + Description: "Time is the timestamp of when the ManagedFields entry was added. The timestamp will also be updated if a field is added, the manager changes any of the owned fields value or removes a field. The timestamp does not update when a field is removed from the entry because another manager took it over.", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "fieldsType": { + SchemaProps: spec.SchemaProps{ + Description: "FieldsType is the discriminator for the different fields format and version. There is currently only one possible value: \"FieldsV1\"", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldsV1": { + SchemaProps: spec.SchemaProps{ + Description: "FieldsV1 holds the first JSON version format as described in the \"FieldsV1\" type.", + Ref: ref(v1.FieldsV1{}.OpenAPIModelName()), + }, + }, + "subresource": { + SchemaProps: spec.SchemaProps{ + Description: "Subresource is the name of the subresource used to update that object, or empty string if the object was updated through the main resource. The value of this field is used to distinguish between managers, even if they share the same name. For example, a status update will be distinct from a regular update using the same manager name. Note that the APIVersion field is not related to the Subresource field and it always corresponds to the version of the main resource.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.FieldsV1{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_MicroTime(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "MicroTime is version of Time with microsecond level precision.", + Type: v1.MicroTime{}.OpenAPISchemaType(), + Format: v1.MicroTime{}.OpenAPISchemaFormat(), + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ObjectMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ObjectMeta is metadata that all persisted resources must have, which includes all objects users must create.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name must be unique within a namespace. Is required when creating resources, although some resources may allow a client to request the generation of an appropriate name automatically. Name is primarily intended for creation idempotence and configuration definition. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + Type: []string{"string"}, + Format: "", + }, + }, + "generateName": { + SchemaProps: spec.SchemaProps{ + Description: "GenerateName is an optional prefix, used by the server, to generate a unique name ONLY IF the Name field has not been provided. If this field is used, the name returned to the client will be different than the name passed. This value will also be combined with a unique suffix. The provided value has the same validation rules as the Name field, and may be truncated by the length of the suffix required to make the value unique on the server.\n\nIf this field is specified and the generated name exists, the server will return a 409.\n\nApplied only if Name is not specified. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#idempotency", + Type: []string{"string"}, + Format: "", + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Description: "Namespace defines the space within which each name must be unique. An empty namespace is equivalent to the \"default\" namespace, but \"default\" is the canonical representation. Not all objects are required to be scoped to a namespace - the value of this field for those objects will be empty.\n\nMust be a DNS_LABEL. Cannot be updated. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces", + Type: []string{"string"}, + Format: "", + }, + }, + "selfLink": { + SchemaProps: spec.SchemaProps{ + Description: "Deprecated: selfLink is a legacy read-only field that is no longer populated by the system.", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID is the unique in time and space value for this object. It is typically generated by the server on successful creation of a resource and is not allowed to change on PUT operations.\n\nPopulated by the system. Read-only. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "An opaque value that represents the internal version of this object that can be used by clients to determine when objects have changed. May be used for optimistic concurrency, change detection, and the watch operation on a resource or set of resources. Clients must treat these values as opaque and passed unmodified back to the server. They may only be valid for a particular resource or set of resources.\n\nPopulated by the system. Read-only. Value must be treated as opaque by clients and . More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + Type: []string{"string"}, + Format: "", + }, + }, + "generation": { + SchemaProps: spec.SchemaProps{ + Description: "A sequence number representing a specific generation of the desired state. Populated by the system. Read-only.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "creationTimestamp": { + SchemaProps: spec.SchemaProps{ + Description: "CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\n\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "deletionTimestamp": { + SchemaProps: spec.SchemaProps{ + Description: "DeletionTimestamp is RFC 3339 date and time at which this resource will be deleted. This field is set by the server when a graceful deletion is requested by the user, and is not directly settable by a client. The resource is expected to be deleted (no longer visible from resource lists, and not reachable by name) after the time in this field, once the finalizers list is empty. As long as the finalizers list contains items, deletion is blocked. Once the deletionTimestamp is set, this value may not be unset or be set further into the future, although it may be shortened or the resource may be deleted prior to this time. For example, a user may request that a pod is deleted in 30 seconds. The Kubelet will react by sending a graceful termination signal to the containers in the pod. After that 30 seconds, the Kubelet will send a hard termination signal (SIGKILL) to the container and after cleanup, remove the pod from the API. In the presence of network partitions, this object may still exist after this timestamp, until an administrator or automated process can determine the resource is fully terminated. If not set, graceful deletion of the object has not been requested.\n\nPopulated by the system when a graceful deletion is requested. Read-only. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Ref: ref(v1.Time{}.OpenAPIModelName()), + }, + }, + "deletionGracePeriodSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "Number of seconds allowed for this object to gracefully terminate before it will be removed from the system. Only set when deletionTimestamp is also set. May only be shortened. Read-only.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + "labels": { + SchemaProps: spec.SchemaProps{ + Description: "Map of string keys and values that can be used to organize and categorize (scope and select) objects. May match selectors of replication controllers and services. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "annotations": { + SchemaProps: spec.SchemaProps{ + Description: "Annotations is an unstructured key value map stored with a resource that may be set by external tools to store and retrieve arbitrary metadata. They are not queryable and should be preserved when modifying objects. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations", + Type: []string{"object"}, + AdditionalProperties: &spec.SchemaOrBool{ + Allows: true, + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "ownerReferences": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-map-keys": []interface{}{ + "uid", + }, + "x-kubernetes-list-type": "map", + "x-kubernetes-patch-merge-key": "uid", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "List of objects depended by this object. If ALL objects in the list have been deleted, this object will be garbage collected. If this object is managed by a controller, then an entry in this list will point to this controller, with the controller field set to true. There cannot be more than one managing controller.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.OwnerReference{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "finalizers": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "set", + "x-kubernetes-patch-strategy": "merge", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "Must be empty before the object is deleted from the registry. Each entry is an identifier for the responsible component that will remove the entry from the list. If the deletionTimestamp of the object is non-nil, entries in this list can only be removed. Finalizers may be processed and removed in any order. Order is NOT enforced because it introduces significant risk of stuck finalizers. finalizers is a shared field, any actor with permission can reorder it. If the finalizer list is processed in order, then this can lead to a situation in which the component responsible for the first finalizer in the list is waiting for a signal (field value, external system, or other) produced by a component responsible for a finalizer later in the list, resulting in a deadlock. Without enforced ordering finalizers are free to order amongst themselves and are not vulnerable to ordering changes in the list.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "managedFields": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "ManagedFields maps workflow-id and version to the set of fields that are managed by that workflow. This is mostly for internal housekeeping, and users typically shouldn't need to set or understand this field. A workflow can be the user's name, a controller's name, or the name of a specific apply path like \"ci-cd\". The set of fields is always in the version that the workflow used when modifying the object.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.ManagedFieldsEntry{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ManagedFieldsEntry{}.OpenAPIModelName(), v1.OwnerReference{}.OpenAPIModelName(), v1.Time{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_OwnerReference(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "OwnerReference contains enough information to let you identify an owning object. An owning object must be in the same namespace as the dependent, or be cluster-scoped, so there is no namespace field.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "API version of the referent.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "name": { + SchemaProps: spec.SchemaProps{ + Description: "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#names", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "controller": { + SchemaProps: spec.SchemaProps{ + Description: "If true, this reference points to the managing controller.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "blockOwnerDeletion": { + SchemaProps: spec.SchemaProps{ + Description: "If true, AND if the owner has the \"foregroundDeletion\" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed. See https://kubernetes.io/docs/concepts/architecture/garbage-collection/#foreground-deletion for how the garbage collector interacts with this field and enforces the foreground deletion. Defaults to false. To set this field, a user needs \"delete\" permission of the owner, otherwise 422 (Unprocessable Entity) will be returned.", + Type: []string{"boolean"}, + Format: "", + }, + }, + }, + Required: []string{"apiVersion", "kind", "name", "uid"}, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-map-type": "atomic", + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_PartialObjectMetadata(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PartialObjectMetadata is a generic representation of any object with ObjectMeta. It allows clients to get access to a particular ObjectMeta schema without knowing the details of the version.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard object's metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata", + Default: map[string]interface{}{}, + Ref: ref(v1.ObjectMeta{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ObjectMeta{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PartialObjectMetadataList contains a list of objects containing only their metadata", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "items": { + SchemaProps: spec.SchemaProps{ + Description: "items contains each of the included items.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.PartialObjectMetadata{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"items"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.PartialObjectMetadata{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_Patch(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Patch is provided to give a concrete name and type to the Kubernetes PATCH request body.", + Type: []string{"object"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_PatchOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "PatchOptions may be provided when patching an API object. PatchOptions is meant to be a superset of UpdateOptions.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "force": { + SchemaProps: spec.SchemaProps{ + Description: "Force is going to \"force\" Apply requests. It means user will re-acquire conflicting fields owned by other people. Force flag must be unset for non-apply patch requests.", + Type: []string{"boolean"}, + Format: "", + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint. This field is required for apply requests (application/apply-patch) but optional for non-apply patch types (JsonPatch, MergePatch, StrategicMergePatch).", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldValidation": { + SchemaProps: spec.SchemaProps{ + Description: "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Preconditions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Preconditions must be fulfilled before an operation (update, delete, etc.) is carried out.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the target UID.", + Type: []string{"string"}, + Format: "", + }, + }, + "resourceVersion": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the target ResourceVersion", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_RootPaths(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "RootPaths lists the paths available at root. For example: \"/healthz\", \"/apis\".", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "paths": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "paths are the paths available at root.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + }, + Required: []string{"paths"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "ServerAddressByClientCIDR helps the client to determine the server address that they should use, depending on the clientCIDR that they match.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "clientCIDR": { + SchemaProps: spec.SchemaProps{ + Description: "The CIDR with which clients can match their IP to figure out the server address that they should use.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "serverAddress": { + SchemaProps: spec.SchemaProps{ + Description: "Address of this server, suitable for a client that matches the above CIDR. This can be a hostname, hostname:port, IP or IP:port.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"clientCIDR", "serverAddress"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Status(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Status is a return value for calls that don't return other objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "Status of the operation. One of: \"Success\" or \"Failure\". More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "A human-readable description of the status of this operation.", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "A machine-readable description of why this operation is in the \"Failure\" status. If this value is empty there is no information available. A Reason clarifies an HTTP status code but does not override it.", + Type: []string{"string"}, + Format: "", + }, + }, + "details": { + SchemaProps: spec.SchemaProps{ + Description: "Extended data associated with the reason. Each reason may define its own extended details. This field is optional and the data returned is not guaranteed to conform to any schema except that defined by the reason type.", + Ref: ref(v1.StatusDetails{}.OpenAPIModelName()), + }, + }, + "code": { + SchemaProps: spec.SchemaProps{ + Description: "Suggested HTTP return code for this status, 0 if not set.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.StatusDetails{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_StatusCause(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StatusCause provides more information about an api.Status failure, including cases when multiple errors are encountered.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "A machine-readable description of the cause of the error. If this value is empty there is no information available.", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "A human-readable description of the cause of the error. This field may be presented as-is to a reader.", + Type: []string{"string"}, + Format: "", + }, + }, + "field": { + SchemaProps: spec.SchemaProps{ + Description: "The field of the resource that has caused this error, as named by its JSON serialization. May include dot and postfix notation for nested attributes. Arrays are zero-indexed. Fields may appear more than once in an array of causes due to fields having multiple errors. Optional.\n\nExamples:\n \"name\" - the field \"name\" on the current resource\n \"items[0].name\" - the field \"name\" on the first array entry in \"items\"", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_StatusDetails(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "StatusDetails is a set of additional properties that MAY be set by the server to provide additional information about a response. The Reason field of a Status object defines what attributes will be set. Clients must ignore fields that do not match the defined type of each attribute, and should assume that any attribute may be empty, invalid, or under defined.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "The name attribute of the resource associated with the status StatusReason (when there is a single name which can be described).", + Type: []string{"string"}, + Format: "", + }, + }, + "group": { + SchemaProps: spec.SchemaProps{ + Description: "The group attribute of the resource associated with the status StatusReason.", + Type: []string{"string"}, + Format: "", + }, + }, + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "The kind attribute of the resource associated with the status StatusReason. On some operations may differ from the requested resource Kind. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "uid": { + SchemaProps: spec.SchemaProps{ + Description: "UID of the resource. (when there is a single resource which can be described). More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names#uids", + Type: []string{"string"}, + Format: "", + }, + }, + "causes": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "The Causes array includes more details associated with the StatusReason failure. Not all StatusReasons may provide detailed causes.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.StatusCause{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "retryAfterSeconds": { + SchemaProps: spec.SchemaProps{ + Description: "If specified, the time in seconds before the operation should be retried. Some errors may indicate the client must take an alternate action - for those errors this field may indicate how long to wait before taking the alternate action.", + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.StatusCause{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_Table(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Table is a tabular representation of a set of API resources. The server transforms the object into a set of preferred columns for quickly reviewing the objects.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "metadata": { + SchemaProps: spec.SchemaProps{ + Description: "Standard list metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Default: map[string]interface{}{}, + Ref: ref(v1.ListMeta{}.OpenAPIModelName()), + }, + }, + "columnDefinitions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "columnDefinitions describes each column in the returned items array. The number of cells per row will always match the number of column definitions.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableColumnDefinition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "rows": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "rows is the list of items in the table.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableRow{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + }, + Required: []string{"columnDefinitions", "rows"}, + }, + }, + Dependencies: []string{ + v1.ListMeta{}.OpenAPIModelName(), v1.TableColumnDefinition{}.OpenAPIModelName(), v1.TableRow{}.OpenAPIModelName()}, + } +} + +func schema_pkg_apis_meta_v1_TableColumnDefinition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableColumnDefinition contains information about a column returned in the Table.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "name": { + SchemaProps: spec.SchemaProps{ + Description: "name is a human readable name for the column.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "type": { + SchemaProps: spec.SchemaProps{ + Description: "type is an OpenAPI type definition for this column, such as number, integer, string, or array. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "format": { + SchemaProps: spec.SchemaProps{ + Description: "format is an optional OpenAPI type modifier for this column. A format modifies the type and imposes additional rules, like date or time formatting for a string. The 'name' format is applied to the primary identifier column which has type 'string' to assist in clients identifying column is the resource name. See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#data-types for more.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "description": { + SchemaProps: spec.SchemaProps{ + Description: "description is a human readable description of this column.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "priority": { + SchemaProps: spec.SchemaProps{ + Description: "priority is an integer defining the relative importance of this column compared to others. Lower numbers are considered higher priority. Columns that may be omitted in limited space scenarios should be given a higher priority.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"name", "type", "format", "description", "priority"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TableOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableOptions are used when a Table is requested by the caller.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "includeObject": { + SchemaProps: spec.SchemaProps{ + Description: "includeObject decides whether to include each object along with its columnar information. Specifying \"None\" will return no object, specifying \"Object\" will return the full object contents, and specifying \"Metadata\" (the default) will return the object's metadata in the PartialObjectMetadata kind in version v1beta1 of the meta.k8s.io API group.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TableRow(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableRow is an individual row in a table.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "cells": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "cells will be as wide as the column definitions array and may contain strings, numbers (float64 or int64), booleans, simple maps, lists, or null. See the type field of the column definition for a more detailed description.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Format: "", + }, + }, + }, + }, + }, + "conditions": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "conditions describe additional status of a row that are relevant for a human user. These conditions apply to the row, not to the object, and will be specific to table output. The only defined condition type is 'Completed', for a row that indicates a resource that has run to completion and can be given less visual priority.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.TableRowCondition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "object": { + SchemaProps: spec.SchemaProps{ + Description: "This field contains the requested additional information about each object based on the includeObject policy when requesting the Table. If \"None\", this field is empty, if \"Object\" this will be the default serialization of the object for the current API version, and if \"Metadata\" (the default) will contain the object metadata. Check the returned kind and apiVersion of the object before parsing. The media type of the object will always match the enclosing list - if this as a JSON table, these will be JSON encoded objects.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + Required: []string{"cells"}, + }, + }, + Dependencies: []string{ + v1.TableRowCondition{}.OpenAPIModelName(), "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_pkg_apis_meta_v1_TableRowCondition(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TableRowCondition allows a row to be marked with additional information.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Description: "Type of row condition. The only defined value is 'Completed' indicating that the object this row represents has reached a completed state and may be given less visual priority than other rows. Clients are not required to honor any conditions but should be consistent where possible about handling the conditions.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "status": { + SchemaProps: spec.SchemaProps{ + Description: "Status of the condition, one of True, False, Unknown.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "reason": { + SchemaProps: spec.SchemaProps{ + Description: "(brief) machine readable reason for the condition's last transition.", + Type: []string{"string"}, + Format: "", + }, + }, + "message": { + SchemaProps: spec.SchemaProps{ + Description: "Human readable message indicating details about last transition.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"type", "status"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Time(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Time is a wrapper around time.Time which supports correct marshaling to YAML and JSON. Wrappers are provided for many of the factory methods that the time package offers.", + Type: v1.Time{}.OpenAPISchemaType(), + Format: v1.Time{}.OpenAPISchemaFormat(), + }, + }, + } +} + +func schema_pkg_apis_meta_v1_Timestamp(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Timestamp is a struct that is equivalent to Time, but intended for protobuf marshalling/unmarshalling. It is generated into a serialization that matches Time. Do not use in Go structs.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "seconds": { + SchemaProps: spec.SchemaProps{ + Description: "Represents seconds of UTC time since Unix epoch 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59Z inclusive.", + Default: 0, + Type: []string{"integer"}, + Format: "int64", + }, + }, + "nanos": { + SchemaProps: spec.SchemaProps{ + Description: "Non-negative fractions of a second at nanosecond resolution. Negative second values with fractions must still have non-negative nanos values that count forward in time. Must be from 0 to 999,999,999 inclusive. This field may be limited in precision depending on context.", + Default: 0, + Type: []string{"integer"}, + Format: "int32", + }, + }, + }, + Required: []string{"seconds", "nanos"}, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_TypeMeta(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "TypeMeta describes an individual object in an API response or request with strings representing the type of the object and its API schema version. Structures that are versioned or persisted should inline TypeMeta.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_UpdateOptions(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "UpdateOptions may be provided when updating an API object. All fields in UpdateOptions should also be present in PatchOptions.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "kind": { + SchemaProps: spec.SchemaProps{ + Description: "Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + Type: []string{"string"}, + Format: "", + }, + }, + "apiVersion": { + SchemaProps: spec.SchemaProps{ + Description: "APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources", + Type: []string{"string"}, + Format: "", + }, + }, + "dryRun": { + VendorExtensible: spec.VendorExtensible{ + Extensions: spec.Extensions{ + "x-kubernetes-list-type": "atomic", + }, + }, + SchemaProps: spec.SchemaProps{ + Description: "When present, indicates that modifications should not be persisted. An invalid or unrecognized dryRun directive will result in an error response and no further processing of the request. Valid values are: - All: all dry run stages will be processed", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + "fieldManager": { + SchemaProps: spec.SchemaProps{ + Description: "fieldManager is a name associated with the actor or entity that is making these changes. The value must be less than or 128 characters long, and only contain printable characters, as defined by https://golang.org/pkg/unicode/#IsPrint.", + Type: []string{"string"}, + Format: "", + }, + }, + "fieldValidation": { + SchemaProps: spec.SchemaProps{ + Description: "fieldValidation instructs the server on how to handle objects in the request (POST/PUT/PATCH) containing unknown or duplicate fields. Valid values are: - Ignore: This will ignore any unknown fields that are silently dropped from the object, and will ignore all but the last duplicate field that the decoder encounters. This is the default behavior prior to v1.23. - Warn: This will send a warning via the standard warning response header for each unknown field that is dropped from the object, and for each duplicate field that is encountered. The request will still succeed if there are no other errors, and will only persist the last of any duplicate fields. This is the default in v1.23+ - Strict: This will fail the request with a BadRequest error if any unknown fields would be dropped from the object, or if any duplicate fields are present. The error returned from the server will contain all unknown and duplicate fields encountered.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + +func schema_pkg_apis_meta_v1_WatchEvent(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Event represents a single event to a watched resource.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "type": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "object": { + SchemaProps: spec.SchemaProps{ + Description: "Object is:\n * If Type is Added or Modified: the new state of the object.\n * If Type is Deleted: the state of the object immediately before deletion.\n * If Type is Error: *Status is recommended; other types may make sense\n depending on context.", + Ref: ref("k8s.io/apimachinery/pkg/runtime.RawExtension"), + }, + }, + }, + Required: []string{"type", "object"}, + }, + }, + Dependencies: []string{ + "k8s.io/apimachinery/pkg/runtime.RawExtension"}, + } +} + +func schema_k8sio_apimachinery_pkg_version_Info(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Description: "Info contains versioning information. how we'll want to distribute that information.", + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "major": { + SchemaProps: spec.SchemaProps{ + Description: "Major is the major version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "minor": { + SchemaProps: spec.SchemaProps{ + Description: "Minor is the minor version of the binary version", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMajor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMajor is the major version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "emulationMinor": { + SchemaProps: spec.SchemaProps{ + Description: "EmulationMinor is the minor version of the emulation version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMajor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMajor is the major version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", + }, + }, + "minCompatibilityMinor": { + SchemaProps: spec.SchemaProps{ + Description: "MinCompatibilityMinor is the minor version of the minimum compatibility version", + Type: []string{"string"}, + Format: "", + }, + }, + "gitVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "gitCommit": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "gitTreeState": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "buildDate": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "goVersion": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "compiler": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "platform": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"major", "minor", "gitVersion", "gitCommit", "gitTreeState", "buildDate", "goVersion", "compiler", "platform"}, + }, + }, + } +} From 39e6d27a46a328bde529075d8ff67a4619a58271 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 25 Feb 2026 20:49:13 +0300 Subject: [PATCH 04/26] refactor: update operator-helm apis Signed-off-by: Ilya Drey --- .../typed/api/v1alpha1/api_client.go | 14 +- .../api/v1alpha1/fake/fake_api_client.go | 12 +- .../v1alpha1/fake/fake_helmclusteraddon.go | 4 +- .../fake/fake_helmclusteraddonchart.go | 4 +- .../fake/fake_helmclusteraddonrepository.go | 52 +++ .../fake/fake_helmclusterrepository.go | 52 --- .../typed/api/v1alpha1/generated_expansion.go | 2 +- .../typed/api/v1alpha1/helmclusteraddon.go | 6 +- .../api/v1alpha1/helmclusteraddonchart.go | 6 +- .../v1alpha1/helmclusteraddonrepository.go | 72 ++++ .../api/v1alpha1/helmclusterrepository.go | 70 ---- .../api/v1alpha1/helmclusteraddon.go | 17 +- .../api/v1alpha1/helmclusteraddonchart.go | 17 +- ...itory.go => helmclusteraddonrepository.go} | 43 ++- .../api/v1alpha1/interface.go | 14 +- .../informers/externalversions/generic.go | 4 +- .../api/v1alpha1/expansion_generated.go | 18 +- .../listers/api/v1alpha1/helmclusteraddon.go | 28 +- .../api/v1alpha1/helmclusteraddonchart.go | 28 +- .../v1alpha1/helmclusteraddonrepository.go | 48 +++ .../api/v1alpha1/helmclusterrepository.go | 70 ---- api/go.mod | 7 +- api/go.sum | 11 +- api/v1alpha1/doc.go | 3 +- api/v1alpha1/helm_cluster_addon.go | 2 + api/v1alpha1/helm_cluster_addon_chart.go | 2 + api/v1alpha1/helm_cluster_addon_repository.go | 34 +- api/v1alpha1/register.go | 17 +- api/v1alpha1/zz_generated.deepcopy.go | 124 +++---- crds/helmclusteraddoncharts.yaml | 2 + ...yaml => helmclusteraddonrepositories.yaml} | 21 +- crds/helmclusteraddons.yaml | 3 +- .../pkg/api/openapi/zz_generated.openapi.go | 310 +++++++++--------- 33 files changed, 520 insertions(+), 597 deletions(-) create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go delete mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go create mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go delete mode 100644 api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go rename api/client/generated/informers/externalversions/api/v1alpha1/{helmclusterrepository.go => helmclusteraddonrepository.go} (56%) create mode 100644 api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go delete mode 100644 api/client/generated/listers/api/v1alpha1/helmclusterrepository.go rename crds/{helmclusterrepositories.yaml => helmclusteraddonrepositories.yaml} (92%) diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go index 2db4d10..29edccb 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/api_client.go @@ -30,7 +30,7 @@ type HelmV1alpha1Interface interface { RESTClient() rest.Interface HelmClusterAddonsGetter HelmClusterAddonChartsGetter - HelmClusterRepositoriesGetter + HelmClusterAddonRepositoriesGetter } // HelmV1alpha1Client is used to interact with features provided by the helm.deckhouse.io group. @@ -38,16 +38,16 @@ type HelmV1alpha1Client struct { restClient rest.Interface } -func (c *HelmV1alpha1Client) HelmClusterAddons(namespace string) HelmClusterAddonInterface { - return newHelmClusterAddons(c, namespace) +func (c *HelmV1alpha1Client) HelmClusterAddons() HelmClusterAddonInterface { + return newHelmClusterAddons(c) } -func (c *HelmV1alpha1Client) HelmClusterAddonCharts(namespace string) HelmClusterAddonChartInterface { - return newHelmClusterAddonCharts(c, namespace) +func (c *HelmV1alpha1Client) HelmClusterAddonCharts() HelmClusterAddonChartInterface { + return newHelmClusterAddonCharts(c) } -func (c *HelmV1alpha1Client) HelmClusterRepositories(namespace string) HelmClusterRepositoryInterface { - return newHelmClusterRepositories(c, namespace) +func (c *HelmV1alpha1Client) HelmClusterAddonRepositories() HelmClusterAddonRepositoryInterface { + return newHelmClusterAddonRepositories(c) } // NewForConfig creates a new HelmV1alpha1Client for the given config. diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go index 892587d..5b3bbb8 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_api_client.go @@ -28,16 +28,16 @@ type FakeHelmV1alpha1 struct { *testing.Fake } -func (c *FakeHelmV1alpha1) HelmClusterAddons(namespace string) v1alpha1.HelmClusterAddonInterface { - return newFakeHelmClusterAddons(c, namespace) +func (c *FakeHelmV1alpha1) HelmClusterAddons() v1alpha1.HelmClusterAddonInterface { + return newFakeHelmClusterAddons(c) } -func (c *FakeHelmV1alpha1) HelmClusterAddonCharts(namespace string) v1alpha1.HelmClusterAddonChartInterface { - return newFakeHelmClusterAddonCharts(c, namespace) +func (c *FakeHelmV1alpha1) HelmClusterAddonCharts() v1alpha1.HelmClusterAddonChartInterface { + return newFakeHelmClusterAddonCharts(c) } -func (c *FakeHelmV1alpha1) HelmClusterRepositories(namespace string) v1alpha1.HelmClusterRepositoryInterface { - return newFakeHelmClusterRepositories(c, namespace) +func (c *FakeHelmV1alpha1) HelmClusterAddonRepositories() v1alpha1.HelmClusterAddonRepositoryInterface { + return newFakeHelmClusterAddonRepositories(c) } // RESTClient returns a RESTClient that is used to communicate diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go index 3e676f6..909f672 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddon.go @@ -30,11 +30,11 @@ type fakeHelmClusterAddons struct { Fake *FakeHelmV1alpha1 } -func newFakeHelmClusterAddons(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterAddonInterface { +func newFakeHelmClusterAddons(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonInterface { return &fakeHelmClusterAddons{ gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddon, *v1alpha1.HelmClusterAddonList]( fake.Fake, - namespace, + "", v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddons"), v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddon"), func() *v1alpha1.HelmClusterAddon { return &v1alpha1.HelmClusterAddon{} }, diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go index 7660143..f0bf23d 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonchart.go @@ -30,11 +30,11 @@ type fakeHelmClusterAddonCharts struct { Fake *FakeHelmV1alpha1 } -func newFakeHelmClusterAddonCharts(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterAddonChartInterface { +func newFakeHelmClusterAddonCharts(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonChartInterface { return &fakeHelmClusterAddonCharts{ gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddonChart, *v1alpha1.HelmClusterAddonChartList]( fake.Fake, - namespace, + "", v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"), v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddonChart"), func() *v1alpha1.HelmClusterAddonChart { return &v1alpha1.HelmClusterAddonChart{} }, diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go new file mode 100644 index 0000000..bee2b28 --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusteraddonrepository.go @@ -0,0 +1,52 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package fake + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" + v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + gentype "k8s.io/client-go/gentype" +) + +// fakeHelmClusterAddonRepositories implements HelmClusterAddonRepositoryInterface +type fakeHelmClusterAddonRepositories struct { + *gentype.FakeClientWithList[*v1alpha1.HelmClusterAddonRepository, *v1alpha1.HelmClusterAddonRepositoryList] + Fake *FakeHelmV1alpha1 +} + +func newFakeHelmClusterAddonRepositories(fake *FakeHelmV1alpha1) apiv1alpha1.HelmClusterAddonRepositoryInterface { + return &fakeHelmClusterAddonRepositories{ + gentype.NewFakeClientWithList[*v1alpha1.HelmClusterAddonRepository, *v1alpha1.HelmClusterAddonRepositoryList]( + fake.Fake, + "", + v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddonrepositories"), + v1alpha1.SchemeGroupVersion.WithKind("HelmClusterAddonRepository"), + func() *v1alpha1.HelmClusterAddonRepository { return &v1alpha1.HelmClusterAddonRepository{} }, + func() *v1alpha1.HelmClusterAddonRepositoryList { return &v1alpha1.HelmClusterAddonRepositoryList{} }, + func(dst, src *v1alpha1.HelmClusterAddonRepositoryList) { dst.ListMeta = src.ListMeta }, + func(list *v1alpha1.HelmClusterAddonRepositoryList) []*v1alpha1.HelmClusterAddonRepository { + return gentype.ToPointerSlice(list.Items) + }, + func(list *v1alpha1.HelmClusterAddonRepositoryList, items []*v1alpha1.HelmClusterAddonRepository) { + list.Items = gentype.FromPointerSlice(items) + }, + ), + fake, + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go deleted file mode 100644 index a0935c7..0000000 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/fake/fake_helmclusterrepository.go +++ /dev/null @@ -1,52 +0,0 @@ -/* -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. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package fake - -import ( - apiv1alpha1 "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/typed/api/v1alpha1" - v1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - gentype "k8s.io/client-go/gentype" -) - -// fakeHelmClusterRepositories implements HelmClusterRepositoryInterface -type fakeHelmClusterRepositories struct { - *gentype.FakeClientWithList[*v1alpha1.HelmClusterRepository, *v1alpha1.HelmClusterRepositoryList] - Fake *FakeHelmV1alpha1 -} - -func newFakeHelmClusterRepositories(fake *FakeHelmV1alpha1, namespace string) apiv1alpha1.HelmClusterRepositoryInterface { - return &fakeHelmClusterRepositories{ - gentype.NewFakeClientWithList[*v1alpha1.HelmClusterRepository, *v1alpha1.HelmClusterRepositoryList]( - fake.Fake, - namespace, - v1alpha1.SchemeGroupVersion.WithResource("helmclusterrepositories"), - v1alpha1.SchemeGroupVersion.WithKind("HelmClusterRepository"), - func() *v1alpha1.HelmClusterRepository { return &v1alpha1.HelmClusterRepository{} }, - func() *v1alpha1.HelmClusterRepositoryList { return &v1alpha1.HelmClusterRepositoryList{} }, - func(dst, src *v1alpha1.HelmClusterRepositoryList) { dst.ListMeta = src.ListMeta }, - func(list *v1alpha1.HelmClusterRepositoryList) []*v1alpha1.HelmClusterRepository { - return gentype.ToPointerSlice(list.Items) - }, - func(list *v1alpha1.HelmClusterRepositoryList, items []*v1alpha1.HelmClusterRepository) { - list.Items = gentype.FromPointerSlice(items) - }, - ), - fake, - } -} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go index 0a5f07e..911c8ea 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/generated_expansion.go @@ -22,4 +22,4 @@ type HelmClusterAddonExpansion interface{} type HelmClusterAddonChartExpansion interface{} -type HelmClusterRepositoryExpansion interface{} +type HelmClusterAddonRepositoryExpansion interface{} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go index dfc0dc8..05aeac5 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddon.go @@ -32,7 +32,7 @@ import ( // HelmClusterAddonsGetter has a method to return a HelmClusterAddonInterface. // A group's client should implement this interface. type HelmClusterAddonsGetter interface { - HelmClusterAddons(namespace string) HelmClusterAddonInterface + HelmClusterAddons() HelmClusterAddonInterface } // HelmClusterAddonInterface has methods to work with HelmClusterAddon resources. @@ -56,13 +56,13 @@ type helmClusterAddons struct { } // newHelmClusterAddons returns a HelmClusterAddons -func newHelmClusterAddons(c *HelmV1alpha1Client, namespace string) *helmClusterAddons { +func newHelmClusterAddons(c *HelmV1alpha1Client) *helmClusterAddons { return &helmClusterAddons{ gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddon, *apiv1alpha1.HelmClusterAddonList]( "helmclusteraddons", c.RESTClient(), scheme.ParameterCodec, - namespace, + "", func() *apiv1alpha1.HelmClusterAddon { return &apiv1alpha1.HelmClusterAddon{} }, func() *apiv1alpha1.HelmClusterAddonList { return &apiv1alpha1.HelmClusterAddonList{} }, ), diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go index 67de76c..8db564d 100644 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonchart.go @@ -32,7 +32,7 @@ import ( // HelmClusterAddonChartsGetter has a method to return a HelmClusterAddonChartInterface. // A group's client should implement this interface. type HelmClusterAddonChartsGetter interface { - HelmClusterAddonCharts(namespace string) HelmClusterAddonChartInterface + HelmClusterAddonCharts() HelmClusterAddonChartInterface } // HelmClusterAddonChartInterface has methods to work with HelmClusterAddonChart resources. @@ -56,13 +56,13 @@ type helmClusterAddonCharts struct { } // newHelmClusterAddonCharts returns a HelmClusterAddonCharts -func newHelmClusterAddonCharts(c *HelmV1alpha1Client, namespace string) *helmClusterAddonCharts { +func newHelmClusterAddonCharts(c *HelmV1alpha1Client) *helmClusterAddonCharts { return &helmClusterAddonCharts{ gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddonChart, *apiv1alpha1.HelmClusterAddonChartList]( "helmclusteraddoncharts", c.RESTClient(), scheme.ParameterCodec, - namespace, + "", func() *apiv1alpha1.HelmClusterAddonChart { return &apiv1alpha1.HelmClusterAddonChart{} }, func() *apiv1alpha1.HelmClusterAddonChartList { return &apiv1alpha1.HelmClusterAddonChartList{} }, ), diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go new file mode 100644 index 0000000..99494aa --- /dev/null +++ b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusteraddonrepository.go @@ -0,0 +1,72 @@ +/* +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. +*/ + +// Code generated by client-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + context "context" + + scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + types "k8s.io/apimachinery/pkg/types" + watch "k8s.io/apimachinery/pkg/watch" + gentype "k8s.io/client-go/gentype" +) + +// HelmClusterAddonRepositoriesGetter has a method to return a HelmClusterAddonRepositoryInterface. +// A group's client should implement this interface. +type HelmClusterAddonRepositoriesGetter interface { + HelmClusterAddonRepositories() HelmClusterAddonRepositoryInterface +} + +// HelmClusterAddonRepositoryInterface has methods to work with HelmClusterAddonRepository resources. +type HelmClusterAddonRepositoryInterface interface { + Create(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + Update(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). + UpdateStatus(ctx context.Context, helmClusterAddonRepository *apiv1alpha1.HelmClusterAddonRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + Delete(ctx context.Context, name string, opts v1.DeleteOptions) error + DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error + Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterAddonRepository, error) + List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterAddonRepositoryList, error) + Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) + Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterAddonRepository, err error) + HelmClusterAddonRepositoryExpansion +} + +// helmClusterAddonRepositories implements HelmClusterAddonRepositoryInterface +type helmClusterAddonRepositories struct { + *gentype.ClientWithList[*apiv1alpha1.HelmClusterAddonRepository, *apiv1alpha1.HelmClusterAddonRepositoryList] +} + +// newHelmClusterAddonRepositories returns a HelmClusterAddonRepositories +func newHelmClusterAddonRepositories(c *HelmV1alpha1Client) *helmClusterAddonRepositories { + return &helmClusterAddonRepositories{ + gentype.NewClientWithList[*apiv1alpha1.HelmClusterAddonRepository, *apiv1alpha1.HelmClusterAddonRepositoryList]( + "helmclusteraddonrepositories", + c.RESTClient(), + scheme.ParameterCodec, + "", + func() *apiv1alpha1.HelmClusterAddonRepository { return &apiv1alpha1.HelmClusterAddonRepository{} }, + func() *apiv1alpha1.HelmClusterAddonRepositoryList { + return &apiv1alpha1.HelmClusterAddonRepositoryList{} + }, + ), + } +} diff --git a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go b/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go deleted file mode 100644 index d0a84e5..0000000 --- a/api/client/generated/clientset/versioned/typed/api/v1alpha1/helmclusterrepository.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -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. -*/ - -// Code generated by client-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - context "context" - - scheme "github.com/deckhouse/operator-helm/api/client/generated/clientset/versioned/scheme" - apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" - types "k8s.io/apimachinery/pkg/types" - watch "k8s.io/apimachinery/pkg/watch" - gentype "k8s.io/client-go/gentype" -) - -// HelmClusterRepositoriesGetter has a method to return a HelmClusterRepositoryInterface. -// A group's client should implement this interface. -type HelmClusterRepositoriesGetter interface { - HelmClusterRepositories(namespace string) HelmClusterRepositoryInterface -} - -// HelmClusterRepositoryInterface has methods to work with HelmClusterRepository resources. -type HelmClusterRepositoryInterface interface { - Create(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.CreateOptions) (*apiv1alpha1.HelmClusterRepository, error) - Update(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterRepository, error) - // Add a +genclient:noStatus comment above the type to avoid generating UpdateStatus(). - UpdateStatus(ctx context.Context, helmClusterRepository *apiv1alpha1.HelmClusterRepository, opts v1.UpdateOptions) (*apiv1alpha1.HelmClusterRepository, error) - Delete(ctx context.Context, name string, opts v1.DeleteOptions) error - DeleteCollection(ctx context.Context, opts v1.DeleteOptions, listOpts v1.ListOptions) error - Get(ctx context.Context, name string, opts v1.GetOptions) (*apiv1alpha1.HelmClusterRepository, error) - List(ctx context.Context, opts v1.ListOptions) (*apiv1alpha1.HelmClusterRepositoryList, error) - Watch(ctx context.Context, opts v1.ListOptions) (watch.Interface, error) - Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *apiv1alpha1.HelmClusterRepository, err error) - HelmClusterRepositoryExpansion -} - -// helmClusterRepositories implements HelmClusterRepositoryInterface -type helmClusterRepositories struct { - *gentype.ClientWithList[*apiv1alpha1.HelmClusterRepository, *apiv1alpha1.HelmClusterRepositoryList] -} - -// newHelmClusterRepositories returns a HelmClusterRepositories -func newHelmClusterRepositories(c *HelmV1alpha1Client, namespace string) *helmClusterRepositories { - return &helmClusterRepositories{ - gentype.NewClientWithList[*apiv1alpha1.HelmClusterRepository, *apiv1alpha1.HelmClusterRepositoryList]( - "helmclusterrepositories", - c.RESTClient(), - scheme.ParameterCodec, - namespace, - func() *apiv1alpha1.HelmClusterRepository { return &apiv1alpha1.HelmClusterRepository{} }, - func() *apiv1alpha1.HelmClusterRepositoryList { return &apiv1alpha1.HelmClusterRepositoryList{} }, - ), - } -} diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go index 40857e6..8a5f46d 100644 --- a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddon.go @@ -42,45 +42,44 @@ type HelmClusterAddonInformer interface { type helmClusterAddonInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string } // NewHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewHelmClusterAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredHelmClusterAddonInformer(client, namespace, resyncPeriod, indexers, nil) +func NewHelmClusterAddonInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonInformer(client, resyncPeriod, indexers, nil) } // NewFilteredHelmClusterAddonInformer constructs a new informer for HelmClusterAddon type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredHelmClusterAddonInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredHelmClusterAddonInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddons(namespace).List(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddons().List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddons(namespace).Watch(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddons().Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddons(namespace).List(ctx, options) + return client.HelmV1alpha1().HelmClusterAddons().List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddons(namespace).Watch(ctx, options) + return client.HelmV1alpha1().HelmClusterAddons().Watch(ctx, options) }, }, client), &operatorhelmapiv1alpha1.HelmClusterAddon{}, @@ -90,7 +89,7 @@ func NewFilteredHelmClusterAddonInformer(client versioned.Interface, namespace s } func (f *helmClusterAddonInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredHelmClusterAddonInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewFilteredHelmClusterAddonInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *helmClusterAddonInformer) Informer() cache.SharedIndexInformer { diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go index 9d82d80..5e14371 100644 --- a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonchart.go @@ -42,45 +42,44 @@ type HelmClusterAddonChartInformer interface { type helmClusterAddonChartInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string } // NewHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewHelmClusterAddonChartInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredHelmClusterAddonChartInformer(client, namespace, resyncPeriod, indexers, nil) +func NewHelmClusterAddonChartInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonChartInformer(client, resyncPeriod, indexers, nil) } // NewFilteredHelmClusterAddonChartInformer constructs a new informer for HelmClusterAddonChart type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredHelmClusterAddonChartInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredHelmClusterAddonChartInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).List(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddonCharts().List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).Watch(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddonCharts().Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).List(ctx, options) + return client.HelmV1alpha1().HelmClusterAddonCharts().List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterAddonCharts(namespace).Watch(ctx, options) + return client.HelmV1alpha1().HelmClusterAddonCharts().Watch(ctx, options) }, }, client), &operatorhelmapiv1alpha1.HelmClusterAddonChart{}, @@ -90,7 +89,7 @@ func NewFilteredHelmClusterAddonChartInformer(client versioned.Interface, namesp } func (f *helmClusterAddonChartInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredHelmClusterAddonChartInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) + return NewFilteredHelmClusterAddonChartInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } func (f *helmClusterAddonChartInformer) Informer() cache.SharedIndexInformer { diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go similarity index 56% rename from api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go rename to api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go index 4a70c37..b314028 100644 --- a/api/client/generated/informers/externalversions/api/v1alpha1/helmclusterrepository.go +++ b/api/client/generated/informers/externalversions/api/v1alpha1/helmclusteraddonrepository.go @@ -32,71 +32,70 @@ import ( cache "k8s.io/client-go/tools/cache" ) -// HelmClusterRepositoryInformer provides access to a shared informer and lister for -// HelmClusterRepositories. -type HelmClusterRepositoryInformer interface { +// HelmClusterAddonRepositoryInformer provides access to a shared informer and lister for +// HelmClusterAddonRepositories. +type HelmClusterAddonRepositoryInformer interface { Informer() cache.SharedIndexInformer - Lister() apiv1alpha1.HelmClusterRepositoryLister + Lister() apiv1alpha1.HelmClusterAddonRepositoryLister } -type helmClusterRepositoryInformer struct { +type helmClusterAddonRepositoryInformer struct { factory internalinterfaces.SharedInformerFactory tweakListOptions internalinterfaces.TweakListOptionsFunc - namespace string } -// NewHelmClusterRepositoryInformer constructs a new informer for HelmClusterRepository type. +// NewHelmClusterAddonRepositoryInformer constructs a new informer for HelmClusterAddonRepository type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewHelmClusterRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { - return NewFilteredHelmClusterRepositoryInformer(client, namespace, resyncPeriod, indexers, nil) +func NewHelmClusterAddonRepositoryInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonRepositoryInformer(client, resyncPeriod, indexers, nil) } -// NewFilteredHelmClusterRepositoryInformer constructs a new informer for HelmClusterRepository type. +// NewFilteredHelmClusterAddonRepositoryInformer constructs a new informer for HelmClusterAddonRepository type. // Always prefer using an informer factory to get a shared informer instead of getting an independent // one. This reduces memory footprint and number of connections to the server. -func NewFilteredHelmClusterRepositoryInformer(client versioned.Interface, namespace string, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { +func NewFilteredHelmClusterAddonRepositoryInformer(client versioned.Interface, resyncPeriod time.Duration, indexers cache.Indexers, tweakListOptions internalinterfaces.TweakListOptionsFunc) cache.SharedIndexInformer { return cache.NewSharedIndexInformer( cache.ToListWatcherWithWatchListSemantics(&cache.ListWatch{ ListFunc: func(options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterRepositories(namespace).List(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddonRepositories().List(context.Background(), options) }, WatchFunc: func(options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterRepositories(namespace).Watch(context.Background(), options) + return client.HelmV1alpha1().HelmClusterAddonRepositories().Watch(context.Background(), options) }, ListWithContextFunc: func(ctx context.Context, options v1.ListOptions) (runtime.Object, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterRepositories(namespace).List(ctx, options) + return client.HelmV1alpha1().HelmClusterAddonRepositories().List(ctx, options) }, WatchFuncWithContext: func(ctx context.Context, options v1.ListOptions) (watch.Interface, error) { if tweakListOptions != nil { tweakListOptions(&options) } - return client.HelmV1alpha1().HelmClusterRepositories(namespace).Watch(ctx, options) + return client.HelmV1alpha1().HelmClusterAddonRepositories().Watch(ctx, options) }, }, client), - &operatorhelmapiv1alpha1.HelmClusterRepository{}, + &operatorhelmapiv1alpha1.HelmClusterAddonRepository{}, resyncPeriod, indexers, ) } -func (f *helmClusterRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { - return NewFilteredHelmClusterRepositoryInformer(client, f.namespace, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) +func (f *helmClusterAddonRepositoryInformer) defaultInformer(client versioned.Interface, resyncPeriod time.Duration) cache.SharedIndexInformer { + return NewFilteredHelmClusterAddonRepositoryInformer(client, resyncPeriod, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc}, f.tweakListOptions) } -func (f *helmClusterRepositoryInformer) Informer() cache.SharedIndexInformer { - return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterRepository{}, f.defaultInformer) +func (f *helmClusterAddonRepositoryInformer) Informer() cache.SharedIndexInformer { + return f.factory.InformerFor(&operatorhelmapiv1alpha1.HelmClusterAddonRepository{}, f.defaultInformer) } -func (f *helmClusterRepositoryInformer) Lister() apiv1alpha1.HelmClusterRepositoryLister { - return apiv1alpha1.NewHelmClusterRepositoryLister(f.Informer().GetIndexer()) +func (f *helmClusterAddonRepositoryInformer) Lister() apiv1alpha1.HelmClusterAddonRepositoryLister { + return apiv1alpha1.NewHelmClusterAddonRepositoryLister(f.Informer().GetIndexer()) } diff --git a/api/client/generated/informers/externalversions/api/v1alpha1/interface.go b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go index 15233c7..e8deecc 100644 --- a/api/client/generated/informers/externalversions/api/v1alpha1/interface.go +++ b/api/client/generated/informers/externalversions/api/v1alpha1/interface.go @@ -28,8 +28,8 @@ type Interface interface { HelmClusterAddons() HelmClusterAddonInformer // HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. HelmClusterAddonCharts() HelmClusterAddonChartInformer - // HelmClusterRepositories returns a HelmClusterRepositoryInformer. - HelmClusterRepositories() HelmClusterRepositoryInformer + // HelmClusterAddonRepositories returns a HelmClusterAddonRepositoryInformer. + HelmClusterAddonRepositories() HelmClusterAddonRepositoryInformer } type version struct { @@ -45,15 +45,15 @@ func New(f internalinterfaces.SharedInformerFactory, namespace string, tweakList // HelmClusterAddons returns a HelmClusterAddonInformer. func (v *version) HelmClusterAddons() HelmClusterAddonInformer { - return &helmClusterAddonInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} + return &helmClusterAddonInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } // HelmClusterAddonCharts returns a HelmClusterAddonChartInformer. func (v *version) HelmClusterAddonCharts() HelmClusterAddonChartInformer { - return &helmClusterAddonChartInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} + return &helmClusterAddonChartInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } -// HelmClusterRepositories returns a HelmClusterRepositoryInformer. -func (v *version) HelmClusterRepositories() HelmClusterRepositoryInformer { - return &helmClusterRepositoryInformer{factory: v.factory, namespace: v.namespace, tweakListOptions: v.tweakListOptions} +// HelmClusterAddonRepositories returns a HelmClusterAddonRepositoryInformer. +func (v *version) HelmClusterAddonRepositories() HelmClusterAddonRepositoryInformer { + return &helmClusterAddonRepositoryInformer{factory: v.factory, tweakListOptions: v.tweakListOptions} } diff --git a/api/client/generated/informers/externalversions/generic.go b/api/client/generated/informers/externalversions/generic.go index d355a0e..aca9bbb 100644 --- a/api/client/generated/informers/externalversions/generic.go +++ b/api/client/generated/informers/externalversions/generic.go @@ -57,8 +57,8 @@ func (f *sharedInformerFactory) ForResource(resource schema.GroupVersionResource return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddons().Informer()}, nil case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddoncharts"): return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddonCharts().Informer()}, nil - case v1alpha1.SchemeGroupVersion.WithResource("helmclusterrepositories"): - return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterRepositories().Informer()}, nil + case v1alpha1.SchemeGroupVersion.WithResource("helmclusteraddonrepositories"): + return &genericInformer{resource: resource.GroupResource(), informer: f.Helm().V1alpha1().HelmClusterAddonRepositories().Informer()}, nil } diff --git a/api/client/generated/listers/api/v1alpha1/expansion_generated.go b/api/client/generated/listers/api/v1alpha1/expansion_generated.go index 9261d48..8e4f30f 100644 --- a/api/client/generated/listers/api/v1alpha1/expansion_generated.go +++ b/api/client/generated/listers/api/v1alpha1/expansion_generated.go @@ -22,22 +22,10 @@ package v1alpha1 // HelmClusterAddonLister. type HelmClusterAddonListerExpansion interface{} -// HelmClusterAddonNamespaceListerExpansion allows custom methods to be added to -// HelmClusterAddonNamespaceLister. -type HelmClusterAddonNamespaceListerExpansion interface{} - // HelmClusterAddonChartListerExpansion allows custom methods to be added to // HelmClusterAddonChartLister. type HelmClusterAddonChartListerExpansion interface{} -// HelmClusterAddonChartNamespaceListerExpansion allows custom methods to be added to -// HelmClusterAddonChartNamespaceLister. -type HelmClusterAddonChartNamespaceListerExpansion interface{} - -// HelmClusterRepositoryListerExpansion allows custom methods to be added to -// HelmClusterRepositoryLister. -type HelmClusterRepositoryListerExpansion interface{} - -// HelmClusterRepositoryNamespaceListerExpansion allows custom methods to be added to -// HelmClusterRepositoryNamespaceLister. -type HelmClusterRepositoryNamespaceListerExpansion interface{} +// HelmClusterAddonRepositoryListerExpansion allows custom methods to be added to +// HelmClusterAddonRepositoryLister. +type HelmClusterAddonRepositoryListerExpansion interface{} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go index 7752bd6..421d60b 100644 --- a/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddon.go @@ -31,8 +31,9 @@ type HelmClusterAddonLister interface { // List lists all HelmClusterAddons in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddon, err error) - // HelmClusterAddons returns an object that can list and get HelmClusterAddons. - HelmClusterAddons(namespace string) HelmClusterAddonNamespaceLister + // Get retrieves the HelmClusterAddon from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddon, error) HelmClusterAddonListerExpansion } @@ -45,26 +46,3 @@ type helmClusterAddonLister struct { func NewHelmClusterAddonLister(indexer cache.Indexer) HelmClusterAddonLister { return &helmClusterAddonLister{listers.New[*apiv1alpha1.HelmClusterAddon](indexer, apiv1alpha1.Resource("helmclusteraddon"))} } - -// HelmClusterAddons returns an object that can list and get HelmClusterAddons. -func (s *helmClusterAddonLister) HelmClusterAddons(namespace string) HelmClusterAddonNamespaceLister { - return helmClusterAddonNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterAddon](s.ResourceIndexer, namespace)} -} - -// HelmClusterAddonNamespaceLister helps list and get HelmClusterAddons. -// All objects returned here must be treated as read-only. -type HelmClusterAddonNamespaceLister interface { - // List lists all HelmClusterAddons in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddon, err error) - // Get retrieves the HelmClusterAddon from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*apiv1alpha1.HelmClusterAddon, error) - HelmClusterAddonNamespaceListerExpansion -} - -// helmClusterAddonNamespaceLister implements the HelmClusterAddonNamespaceLister -// interface. -type helmClusterAddonNamespaceLister struct { - listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddon] -} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go index 6bfc83c..ee09591 100644 --- a/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddonchart.go @@ -31,8 +31,9 @@ type HelmClusterAddonChartLister interface { // List lists all HelmClusterAddonCharts in the indexer. // Objects returned here must be treated as read-only. List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonChart, err error) - // HelmClusterAddonCharts returns an object that can list and get HelmClusterAddonCharts. - HelmClusterAddonCharts(namespace string) HelmClusterAddonChartNamespaceLister + // Get retrieves the HelmClusterAddonChart from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddonChart, error) HelmClusterAddonChartListerExpansion } @@ -45,26 +46,3 @@ type helmClusterAddonChartLister struct { func NewHelmClusterAddonChartLister(indexer cache.Indexer) HelmClusterAddonChartLister { return &helmClusterAddonChartLister{listers.New[*apiv1alpha1.HelmClusterAddonChart](indexer, apiv1alpha1.Resource("helmclusteraddonchart"))} } - -// HelmClusterAddonCharts returns an object that can list and get HelmClusterAddonCharts. -func (s *helmClusterAddonChartLister) HelmClusterAddonCharts(namespace string) HelmClusterAddonChartNamespaceLister { - return helmClusterAddonChartNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterAddonChart](s.ResourceIndexer, namespace)} -} - -// HelmClusterAddonChartNamespaceLister helps list and get HelmClusterAddonCharts. -// All objects returned here must be treated as read-only. -type HelmClusterAddonChartNamespaceLister interface { - // List lists all HelmClusterAddonCharts in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonChart, err error) - // Get retrieves the HelmClusterAddonChart from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*apiv1alpha1.HelmClusterAddonChart, error) - HelmClusterAddonChartNamespaceListerExpansion -} - -// helmClusterAddonChartNamespaceLister implements the HelmClusterAddonChartNamespaceLister -// interface. -type helmClusterAddonChartNamespaceLister struct { - listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonChart] -} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go b/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go new file mode 100644 index 0000000..5577c5f --- /dev/null +++ b/api/client/generated/listers/api/v1alpha1/helmclusteraddonrepository.go @@ -0,0 +1,48 @@ +/* +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. +*/ + +// Code generated by lister-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + labels "k8s.io/apimachinery/pkg/labels" + listers "k8s.io/client-go/listers" + cache "k8s.io/client-go/tools/cache" +) + +// HelmClusterAddonRepositoryLister helps list HelmClusterAddonRepositories. +// All objects returned here must be treated as read-only. +type HelmClusterAddonRepositoryLister interface { + // List lists all HelmClusterAddonRepositories in the indexer. + // Objects returned here must be treated as read-only. + List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterAddonRepository, err error) + // Get retrieves the HelmClusterAddonRepository from the index for a given name. + // Objects returned here must be treated as read-only. + Get(name string) (*apiv1alpha1.HelmClusterAddonRepository, error) + HelmClusterAddonRepositoryListerExpansion +} + +// helmClusterAddonRepositoryLister implements the HelmClusterAddonRepositoryLister interface. +type helmClusterAddonRepositoryLister struct { + listers.ResourceIndexer[*apiv1alpha1.HelmClusterAddonRepository] +} + +// NewHelmClusterAddonRepositoryLister returns a new HelmClusterAddonRepositoryLister. +func NewHelmClusterAddonRepositoryLister(indexer cache.Indexer) HelmClusterAddonRepositoryLister { + return &helmClusterAddonRepositoryLister{listers.New[*apiv1alpha1.HelmClusterAddonRepository](indexer, apiv1alpha1.Resource("helmclusteraddonrepository"))} +} diff --git a/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go b/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go deleted file mode 100644 index 636260c..0000000 --- a/api/client/generated/listers/api/v1alpha1/helmclusterrepository.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -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. -*/ - -// Code generated by lister-gen. DO NOT EDIT. - -package v1alpha1 - -import ( - apiv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - labels "k8s.io/apimachinery/pkg/labels" - listers "k8s.io/client-go/listers" - cache "k8s.io/client-go/tools/cache" -) - -// HelmClusterRepositoryLister helps list HelmClusterRepositories. -// All objects returned here must be treated as read-only. -type HelmClusterRepositoryLister interface { - // List lists all HelmClusterRepositories in the indexer. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterRepository, err error) - // HelmClusterRepositories returns an object that can list and get HelmClusterRepositories. - HelmClusterRepositories(namespace string) HelmClusterRepositoryNamespaceLister - HelmClusterRepositoryListerExpansion -} - -// helmClusterRepositoryLister implements the HelmClusterRepositoryLister interface. -type helmClusterRepositoryLister struct { - listers.ResourceIndexer[*apiv1alpha1.HelmClusterRepository] -} - -// NewHelmClusterRepositoryLister returns a new HelmClusterRepositoryLister. -func NewHelmClusterRepositoryLister(indexer cache.Indexer) HelmClusterRepositoryLister { - return &helmClusterRepositoryLister{listers.New[*apiv1alpha1.HelmClusterRepository](indexer, apiv1alpha1.Resource("helmclusterrepository"))} -} - -// HelmClusterRepositories returns an object that can list and get HelmClusterRepositories. -func (s *helmClusterRepositoryLister) HelmClusterRepositories(namespace string) HelmClusterRepositoryNamespaceLister { - return helmClusterRepositoryNamespaceLister{listers.NewNamespaced[*apiv1alpha1.HelmClusterRepository](s.ResourceIndexer, namespace)} -} - -// HelmClusterRepositoryNamespaceLister helps list and get HelmClusterRepositories. -// All objects returned here must be treated as read-only. -type HelmClusterRepositoryNamespaceLister interface { - // List lists all HelmClusterRepositories in the indexer for a given namespace. - // Objects returned here must be treated as read-only. - List(selector labels.Selector) (ret []*apiv1alpha1.HelmClusterRepository, err error) - // Get retrieves the HelmClusterRepository from the indexer for a given namespace and name. - // Objects returned here must be treated as read-only. - Get(name string) (*apiv1alpha1.HelmClusterRepository, error) - HelmClusterRepositoryNamespaceListerExpansion -} - -// helmClusterRepositoryNamespaceLister implements the HelmClusterRepositoryNamespaceLister -// interface. -type helmClusterRepositoryNamespaceLister struct { - listers.ResourceIndexer[*apiv1alpha1.HelmClusterRepository] -} diff --git a/api/go.mod b/api/go.mod index 0231311..52759ce 100644 --- a/api/go.mod +++ b/api/go.mod @@ -15,7 +15,7 @@ require ( ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -26,6 +26,7 @@ require ( github.com/gobuffalo/flect v1.0.3 // 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-20250630185457-6e76a2b096b5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -37,7 +38,7 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.3 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -61,7 +62,7 @@ require ( k8s.io/code-generator v0.35.1 // indirect k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/controller-tools v0.17.2 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect diff --git a/api/go.sum b/api/go.sum index 6a95e49..0555393 100644 --- a/api/go.sum +++ b/api/go.sum @@ -3,8 +3,8 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= @@ -32,8 +32,7 @@ github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7O 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= 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= @@ -72,8 +71,8 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -96,7 +95,6 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -155,8 +153,7 @@ k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYo k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= 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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg= diff --git a/api/v1alpha1/doc.go b/api/v1alpha1/doc.go index 51d19b9..11a8472 100644 --- a/api/v1alpha1/doc.go +++ b/api/v1alpha1/doc.go @@ -14,9 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ +// Package v1alpha1 is the v1alpha1 version of the API. // +k8s:deepcopy-gen=package // +k8s:openapi-gen=true - -// Package v1alpha1 is the v1alpha1 version of the API. // +groupName=helm.deckhouse.io package v1alpha1 diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index 16dfd37..e59b818 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -29,12 +29,14 @@ const ( // HelmClusterAddon represents a Helm addon that is installed across the whole cluster. // // +kubebuilder:object:root=true +// +kubebuilder:subresource:status // +kubebuilder:metadata:labels={heritage=deckhouse,module=operator-helm} // +kubebuilder:resource:categories={all,operator-helm},singular=helmclusteraddon,scope=Cluster // +kubebuilder:printcolumn:name="Chart Name",type="string",JSONPath=".spec.chart.helmClusterAddonChart",description="Helm release chart name." // +kubebuilder:printcolumn:name="Chart Version",type="string",JSONPath=".spec.chart.version",description="Helm release chart version." // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="The readiness status of the repository" // +genclient +// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type HelmClusterAddon struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha1/helm_cluster_addon_chart.go b/api/v1alpha1/helm_cluster_addon_chart.go index b9a7bb8..9a87e82 100644 --- a/api/v1alpha1/helm_cluster_addon_chart.go +++ b/api/v1alpha1/helm_cluster_addon_chart.go @@ -28,9 +28,11 @@ const ( // HelmClusterAddonChart represents a Helm chart from specific repository. // // +kubebuilder:object:root=true +// +kubebuilder:subresource:status // +kubebuilder:metadata:labels={heritage=deckhouse,module=operator-helm} // +kubebuilder:resource:categories={all,operator-helm},singular=helmclusteraddonchart,scope=Cluster // +genclient +// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type HelmClusterAddonChart struct { metav1.TypeMeta `json:",inline"` diff --git a/api/v1alpha1/helm_cluster_addon_repository.go b/api/v1alpha1/helm_cluster_addon_repository.go index bdd3c7a..44d37cb 100644 --- a/api/v1alpha1/helm_cluster_addon_repository.go +++ b/api/v1alpha1/helm_cluster_addon_repository.go @@ -21,36 +21,38 @@ import ( ) const ( - HelmClusterRepostoryKind = "HelmClusterRepository" - HelmClusterRepositoryResource = "helmclusterrepositories" + HelmClusterAddonRepostoryKind = "HelmClusterAddonRepository" + HelmClusterAddonRepositoryResource = "helmclusteraddonrepositories" ) -// HelmClusterRepository represens a Git, Helm or OCI complient repocitory with Helm charts. +// HelmClusterAddonRepository represens a Git, Helm or OCI complient repocitory with Helm charts. // // +kubebuilder:object:root=true +// +kubebuilder:subresource:status // +kubebuilder:metadata:labels={heritage=deckhouse,module=operator-helm} -// +kubebuilder:resource:categories={all,operator-helm},singular=helmclusterrepository,scope=Cluster +// +kubebuilder:resource:categories={all,operator-helm},singular=helmclusteraddonrepository,scope=Cluster // +kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status",description="The readiness status of the repository" // +genclient +// +genclient:nonNamespaced // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type HelmClusterRepository struct { +type HelmClusterAddonRepository struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec HelmClusterRepositorySpec `json:"spec"` + Spec HelmClusterAddonRepositorySpec `json:"spec"` // +kubebuilder:default:={"observedGeneration":-1} - Status HelmClusterRepositoryStatus `json:"status,omitempty"` + Status HelmClusterAddonRepositoryStatus `json:"status,omitempty"` } -type HelmClusterRepositorySpec struct { - // URL of the Helm repository. Supports http(s)://, ssh:// and oci:// protocols. +type HelmClusterAddonRepositorySpec struct { + // URL of the Helm repository. Supports http(s):// and oci:// protocols. // +kubebuilder:validation:Required - // +kubebuilder:validation:Pattern=`^(https?|oci|ssh)://.*$` + // +kubebuilder:validation:Pattern=`^(https?|oci)://.*$` URL string `json:"url"` // Auth contains authentication credentials for the repository. // +optional - Auth *HelmClusterRepositoryAuth `json:"auth,omitempty"` + Auth *HelmClusterAddonRepositoryAuth `json:"auth,omitempty"` // CACertificate is the PEM encoded CA certificate for TLS verification. // +optional @@ -64,9 +66,9 @@ type HelmClusterRepositorySpec struct { // TODO: define authentication requirements depeding on registry type -type HelmClusterRepositoryAuth struct{} +type HelmClusterAddonRepositoryAuth struct{} -type HelmClusterRepositoryStatus struct { +type HelmClusterAddonRepositoryStatus struct { // Conditions represent the latest available observations of the repository state. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -74,13 +76,13 @@ type HelmClusterRepositoryStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } -// HelmClusterRepositoryList contains a list of HelmClusterRepositories. +// HelmClusterAddonRepositoryList contains a list of HelmClusterRepositories. // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object -type HelmClusterRepositoryList struct { +type HelmClusterAddonRepositoryList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata"` // Items provides a list of HelmClusterRepositories. - Items []HelmClusterRepository `json:"items"` + Items []HelmClusterAddonRepository `json:"items"` } diff --git a/api/v1alpha1/register.go b/api/v1alpha1/register.go index 5e9064f..df4d426 100644 --- a/api/v1alpha1/register.go +++ b/api/v1alpha1/register.go @@ -30,17 +30,15 @@ const ( var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} var ( - HelmClusterAddonGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} - HelmClusterRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterRepostoryKind} - HelmCluserAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} + HelmClusterAddonGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} + HelmClusterAddonRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonRepostoryKind} + HelmCluserAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} ) -// Kind takes an unqualified kind and returns back a Group qualified GroupKind func Kind(kind string) schema.GroupKind { return SchemeGroupVersion.WithKind(kind).GroupKind() } -// Resource takes an unqualified resource and returns a Group qualified GroupResource func Resource(resource string) schema.GroupResource { return SchemeGroupVersion.WithResource(resource).GroupResource() } @@ -50,19 +48,16 @@ func GroupVersionResource(resource string) schema.GroupVersionResource { } var ( - // SchemeBuilder tbd SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) - // AddToScheme tbd - AddToScheme = SchemeBuilder.AddToScheme + AddToScheme = SchemeBuilder.AddToScheme ) -// Adds the list of known types to Scheme. func addKnownTypes(scheme *runtime.Scheme) error { scheme.AddKnownTypes(SchemeGroupVersion, &HelmClusterAddon{}, &HelmClusterAddonList{}, - &HelmClusterRepository{}, - &HelmClusterRepositoryList{}, + &HelmClusterAddonRepository{}, + &HelmClusterAddonRepositoryList{}, &HelmClusterAddonChart{}, &HelmClusterAddonChartList{}, ) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 901a539..e71c9f4 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -225,72 +225,76 @@ func (in *HelmClusterAddonList) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterAddonSpec) DeepCopyInto(out *HelmClusterAddonSpec) { +func (in *HelmClusterAddonRepository) DeepCopyInto(out *HelmClusterAddonRepository) { *out = *in - out.Chart = in.Chart - if in.Values != nil { - in, out := &in.Values, &out.Values - *out = new(apiextensionsv1.JSON) - (*in).DeepCopyInto(*out) - } + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonSpec. -func (in *HelmClusterAddonSpec) DeepCopy() *HelmClusterAddonSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonRepository. +func (in *HelmClusterAddonRepository) DeepCopy() *HelmClusterAddonRepository { if in == nil { return nil } - out := new(HelmClusterAddonSpec) + out := new(HelmClusterAddonRepository) in.DeepCopyInto(out) return out } +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *HelmClusterAddonRepository) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterAddonStatus) DeepCopyInto(out *HelmClusterAddonStatus) { +func (in *HelmClusterAddonRepositoryAuth) DeepCopyInto(out *HelmClusterAddonRepositoryAuth) { *out = *in - if in.Conditions != nil { - in, out := &in.Conditions, &out.Conditions - *out = make([]v1.Condition, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonStatus. -func (in *HelmClusterAddonStatus) DeepCopy() *HelmClusterAddonStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonRepositoryAuth. +func (in *HelmClusterAddonRepositoryAuth) DeepCopy() *HelmClusterAddonRepositoryAuth { if in == nil { return nil } - out := new(HelmClusterAddonStatus) + out := new(HelmClusterAddonRepositoryAuth) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterRepository) DeepCopyInto(out *HelmClusterRepository) { +func (in *HelmClusterAddonRepositoryList) DeepCopyInto(out *HelmClusterAddonRepositoryList) { *out = *in out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.Status) + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]HelmClusterAddonRepository, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterRepository. -func (in *HelmClusterRepository) DeepCopy() *HelmClusterRepository { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonRepositoryList. +func (in *HelmClusterAddonRepositoryList) DeepCopy() *HelmClusterAddonRepositoryList { if in == nil { return nil } - out := new(HelmClusterRepository) + out := new(HelmClusterAddonRepositoryList) in.DeepCopyInto(out) return out } // DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *HelmClusterRepository) DeepCopyObject() runtime.Object { +func (in *HelmClusterAddonRepositoryList) DeepCopyObject() runtime.Object { if c := in.DeepCopy(); c != nil { return c } @@ -298,29 +302,32 @@ func (in *HelmClusterRepository) DeepCopyObject() runtime.Object { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterRepositoryAuth) DeepCopyInto(out *HelmClusterRepositoryAuth) { +func (in *HelmClusterAddonRepositorySpec) DeepCopyInto(out *HelmClusterAddonRepositorySpec) { *out = *in + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(HelmClusterAddonRepositoryAuth) + **out = **in + } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterRepositoryAuth. -func (in *HelmClusterRepositoryAuth) DeepCopy() *HelmClusterRepositoryAuth { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonRepositorySpec. +func (in *HelmClusterAddonRepositorySpec) DeepCopy() *HelmClusterAddonRepositorySpec { if in == nil { return nil } - out := new(HelmClusterRepositoryAuth) + out := new(HelmClusterAddonRepositorySpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterRepositoryList) DeepCopyInto(out *HelmClusterRepositoryList) { +func (in *HelmClusterAddonRepositoryStatus) DeepCopyInto(out *HelmClusterAddonRepositoryStatus) { *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]HelmClusterRepository, len(*in)) + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -328,47 +335,40 @@ func (in *HelmClusterRepositoryList) DeepCopyInto(out *HelmClusterRepositoryList return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterRepositoryList. -func (in *HelmClusterRepositoryList) DeepCopy() *HelmClusterRepositoryList { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonRepositoryStatus. +func (in *HelmClusterAddonRepositoryStatus) DeepCopy() *HelmClusterAddonRepositoryStatus { if in == nil { return nil } - out := new(HelmClusterRepositoryList) + out := new(HelmClusterAddonRepositoryStatus) in.DeepCopyInto(out) return out } -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *HelmClusterRepositoryList) DeepCopyObject() runtime.Object { - if c := in.DeepCopy(); c != nil { - return c - } - return nil -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterRepositorySpec) DeepCopyInto(out *HelmClusterRepositorySpec) { +func (in *HelmClusterAddonSpec) DeepCopyInto(out *HelmClusterAddonSpec) { *out = *in - if in.Auth != nil { - in, out := &in.Auth, &out.Auth - *out = new(HelmClusterRepositoryAuth) - **out = **in + out.Chart = in.Chart + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) } return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterRepositorySpec. -func (in *HelmClusterRepositorySpec) DeepCopy() *HelmClusterRepositorySpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonSpec. +func (in *HelmClusterAddonSpec) DeepCopy() *HelmClusterAddonSpec { if in == nil { return nil } - out := new(HelmClusterRepositorySpec) + out := new(HelmClusterAddonSpec) in.DeepCopyInto(out) return out } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *HelmClusterRepositoryStatus) DeepCopyInto(out *HelmClusterRepositoryStatus) { +func (in *HelmClusterAddonStatus) DeepCopyInto(out *HelmClusterAddonStatus) { *out = *in if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions @@ -380,12 +380,12 @@ func (in *HelmClusterRepositoryStatus) DeepCopyInto(out *HelmClusterRepositorySt return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterRepositoryStatus. -func (in *HelmClusterRepositoryStatus) DeepCopy() *HelmClusterRepositoryStatus { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonStatus. +func (in *HelmClusterAddonStatus) DeepCopy() *HelmClusterAddonStatus { if in == nil { return nil } - out := new(HelmClusterRepositoryStatus) + out := new(HelmClusterAddonStatus) in.DeepCopyInto(out) return out } diff --git a/crds/helmclusteraddoncharts.yaml b/crds/helmclusteraddoncharts.yaml index 7a24496..cf37e81 100644 --- a/crds/helmclusteraddoncharts.yaml +++ b/crds/helmclusteraddoncharts.yaml @@ -120,3 +120,5 @@ spec: type: object served: true storage: true + subresources: + status: {} diff --git a/crds/helmclusterrepositories.yaml b/crds/helmclusteraddonrepositories.yaml similarity index 92% rename from crds/helmclusterrepositories.yaml rename to crds/helmclusteraddonrepositories.yaml index 4d25b34..d69a87c 100644 --- a/crds/helmclusterrepositories.yaml +++ b/crds/helmclusteraddonrepositories.yaml @@ -7,17 +7,17 @@ metadata: labels: heritage: deckhouse module: operator-helm - name: helmclusterrepositories.helm.deckhouse.io + name: helmclusteraddonrepositories.helm.deckhouse.io spec: group: helm.deckhouse.io names: categories: - all - operator-helm - kind: HelmClusterRepository - listKind: HelmClusterRepositoryList - plural: helmclusterrepositories - singular: helmclusterrepository + kind: HelmClusterAddonRepository + listKind: HelmClusterAddonRepositoryList + plural: helmclusteraddonrepositories + singular: helmclusteraddonrepository scope: Cluster versions: - additionalPrinterColumns: @@ -28,7 +28,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: HelmClusterRepository represens a Git, Helm or OCI complient + description: HelmClusterAddonRepository represens a Git, Helm or OCI complient repocitory with Helm charts. properties: apiVersion: @@ -62,9 +62,9 @@ spec: description: TLSVerify enables or disables TLS certificate verification. type: boolean url: - description: URL of the Helm repository. Supports http(s)://, ssh:// - and oci:// protocols. - pattern: ^(https?|oci|ssh)://.*$ + description: URL of the Helm repository. Supports http(s):// and oci:// + protocols. + pattern: ^(https?|oci)://.*$ type: string required: - url @@ -142,4 +142,5 @@ spec: type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml index 2afbf5c..8f92b6f 100644 --- a/crds/helmclusteraddons.yaml +++ b/crds/helmclusteraddons.yaml @@ -158,4 +158,5 @@ spec: type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index 1bd0664..e1148ba 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -30,71 +30,71 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepository": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepository(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryAuth": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryAuth(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositorySpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositorySpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryStatus(ref), - v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), - v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), - v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), - v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), - v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), - v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), - v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), - v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), - v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), - v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), - v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), - v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), - v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), - v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), - v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), - v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), - v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), - v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), - v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), - v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), - v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), - v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), - v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), - v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), - v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), - v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), - v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), - v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), - v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), - v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), - v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), - v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), - v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), - v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), - v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), - v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), - v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), - v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), - v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), - v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), - v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), - v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), - version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), + v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), + v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), + v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), + v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), + v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), + v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), + v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), + v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), + v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), + v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), + v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), + v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), + v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), + v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), + v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), + v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), + v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), + v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), + v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), + v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), + v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), + v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), + v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), + v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), + v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), + v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), + v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), + v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), + v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), + v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), + v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), + v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), + v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), + v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), + v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), + v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), + v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), + v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), + v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), + v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), + v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), + version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), } } @@ -416,86 +416,11 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref common } } -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "chart": { - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef"), - }, - }, - "values": { - SchemaProps: spec.SchemaProps{ - Description: "Values holds the values for this Helm release.", - Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), - }, - }, - "namespace": { - SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", - }, - }, - "maintanace": { - SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", - }, - }, - }, - Required: []string{"chart"}, - }, - }, - Dependencies: []string{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"}, - } -} - -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { - return common.OpenAPIDefinition{ - Schema: spec.Schema{ - SchemaProps: spec.SchemaProps{ - Type: []string{"object"}, - Properties: map[string]spec.Schema{ - "conditions": { - SchemaProps: spec.SchemaProps{ - Description: "Conditions represent the latest available observations of the repository state.", - Type: []string{"array"}, - Items: &spec.SchemaOrArray{ - Schema: &spec.Schema{ - SchemaProps: spec.SchemaProps{ - Default: map[string]interface{}{}, - Ref: ref(v1.Condition{}.OpenAPIModelName()), - }, - }, - }, - }, - }, - "observedGeneration": { - SchemaProps: spec.SchemaProps{ - Description: "Generating a resource that was last processed by the controller.", - Type: []string{"integer"}, - Format: "int64", - }, - }, - }, - }, - }, - Dependencies: []string{ - v1.Condition{}.OpenAPIModelName()}, - } -} - -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepository(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "HelmClusterRepository represens a Git, Helm or OCI complient repocitory with Helm charts.", + Description: "HelmClusterAddonRepository represens a Git, Helm or OCI complient repocitory with Helm charts.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -521,13 +446,13 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepository(ref commo "spec": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositorySpec"), + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec"), }, }, "status": { SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryStatus"), + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus"), }, }, }, @@ -535,11 +460,11 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepository(ref commo }, }, Dependencies: []string{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositorySpec", "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryStatus", v1.ObjectMeta{}.OpenAPIModelName()}, + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec", "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus", v1.ObjectMeta{}.OpenAPIModelName()}, } } -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryAuth(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -549,11 +474,11 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryAuth(ref c } } -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "HelmClusterRepositoryList contains a list of HelmClusterRepositories.", + Description: "HelmClusterAddonRepositoryList contains a list of HelmClusterRepositories.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -584,7 +509,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryList(ref c Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ Default: map[string]interface{}{}, - Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepository"), + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository"), }, }, }, @@ -595,11 +520,11 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryList(ref c }, }, Dependencies: []string{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepository", v1.ListMeta{}.OpenAPIModelName()}, + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository", v1.ListMeta{}.OpenAPIModelName()}, } } -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositorySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -607,7 +532,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositorySpec(ref c Properties: map[string]spec.Schema{ "url": { SchemaProps: spec.SchemaProps{ - Description: "URL of the Helm repository. Supports http(s)://, ssh:// and oci:// protocols.", + Description: "URL of the Helm repository. Supports http(s):// and oci:// protocols.", Default: "", Type: []string{"string"}, Format: "", @@ -616,7 +541,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositorySpec(ref c "auth": { SchemaProps: spec.SchemaProps{ Description: "Auth contains authentication credentials for the repository.", - Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryAuth"), + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth"), }, }, "caCertificate": { @@ -638,11 +563,86 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositorySpec(ref c }, }, Dependencies: []string{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterRepositoryAuth"}, + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth"}, } } -func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterRepositoryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "conditions": { + SchemaProps: spec.SchemaProps{ + Description: "Conditions represent the latest available observations of the repository state.", + Type: []string{"array"}, + Items: &spec.SchemaOrArray{ + Schema: &spec.Schema{ + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref(v1.Condition{}.OpenAPIModelName()), + }, + }, + }, + }, + }, + "observedGeneration": { + SchemaProps: spec.SchemaProps{ + Description: "Generating a resource that was last processed by the controller.", + Type: []string{"integer"}, + Format: "int64", + }, + }, + }, + }, + }, + Dependencies: []string{ + v1.Condition{}.OpenAPIModelName()}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "chart": { + SchemaProps: spec.SchemaProps{ + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef"), + }, + }, + "values": { + SchemaProps: spec.SchemaProps{ + Description: "Values holds the values for this Helm release.", + Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), + }, + }, + "namespace": { + SchemaProps: spec.SchemaProps{ + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "maintanace": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"chart"}, + }, + }, + Dependencies: []string{ + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"}, + } +} + +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ From f6bcbac1904196cf6c02b2b3ef333e60bf784130 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 25 Feb 2026 21:18:47 +0300 Subject: [PATCH 05/26] feat: implement basic repository management capabilities Signed-off-by: Ilya Drey --- images/distroless/werf.inc.yaml | 53 +++++ .../cmd/operator-helm-controller/main.go | 97 ++++++++ images/operator-helm-artifact/go.mod | 44 +++- images/operator-helm-artifact/go.sum | 112 ++++----- .../helmclusteraddonrepository/constants.go | 58 +++++ .../helmclusteraddonrepository/controller.go | 84 +++++++ .../helmclusteraddonrepository/mapper.go | 147 ++++++++++++ .../helmclusteraddonrepository/reconciler.go | 175 ++++++++++++++ images/operator-helm-artifact/werf.inc.yaml | 57 +++++ .../mount-points.yaml | 1 + images/operator-helm-controller/werf.inc.yaml | 16 ++ .../operator-helm-controller/deployment.yaml | 133 +++++++++++ .../operator-helm-controller/rbac-for-us.yaml | 216 ++++++++++++++++++ .../service-metrics.yaml | 16 ++ .../service-monitor.yaml | 24 ++ templates/rbac-to-us.yaml | 2 +- 16 files changed, 1167 insertions(+), 68 deletions(-) create mode 100644 images/distroless/werf.inc.yaml create mode 100644 images/operator-helm-artifact/cmd/operator-helm-controller/main.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go create mode 100644 images/operator-helm-artifact/werf.inc.yaml create mode 100644 images/operator-helm-controller/mount-points.yaml create mode 100644 images/operator-helm-controller/werf.inc.yaml create mode 100644 templates/operator-helm-controller/deployment.yaml create mode 100644 templates/operator-helm-controller/rbac-for-us.yaml create mode 100644 templates/operator-helm-controller/service-metrics.yaml create mode 100644 templates/operator-helm-controller/service-monitor.yaml diff --git a/images/distroless/werf.inc.yaml b/images/distroless/werf.inc.yaml new file mode 100644 index 0000000..99f6e11 --- /dev/null +++ b/images/distroless/werf.inc.yaml @@ -0,0 +1,53 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-artifact +fromImage: builder/alt +final: false +shell: + beforeInstall: + {{- include "alt packages proxy" . | nindent 2 }} + - | + apt-get install ca-certificates tzdata -y + {{- include "alt packages clean" . | nindent 2 }} + install: + - | + mkdir -p /relocate/etc/{pki,ssl} /relocate/usr/{bin,sbin,share,lib,lib64} + + cd /relocate + for dir in {bin,sbin,lib,lib64};do + ln -s usr/$dir $dir + done + # /var/run -> ../run symlink to prevent making /var/run a directory during the build. + # It is needed for better compatibility with containerd default top layer. + mkdir -p run + mkdir -p var + ln -s var/run ../run + cd / + + cp -pr /tmp /relocate + cp -pr /etc/passwd /etc/group /etc/hostname /etc/hosts /etc/shadow /etc/protocols /etc/services /etc/nsswitch.conf /relocate/etc + cp -pr /usr/share/ca-certificates /relocate/usr/share + cp -pr /usr/share/zoneinfo /relocate/usr/share + cp -pr /etc/pki/tls/cert.pem /relocate/etc/ssl + cp -pr /etc/pki/tls/certs /relocate/etc/ssl + cp -pr /etc/pki/ca-trust/ /relocate/etc/ + # Create 'deckhouse' user to run without root. + echo "deckhouse:x:64535:64535:deckhouse:/:/sbin/nologin" >> /relocate/etc/passwd + echo "deckhouse:x:64535:" >> /relocate/etc/group + echo "deckhouse:!::0:::::" >> /relocate/etc/shadow +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +final: false +fromImage: builder/scratch +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-artifact + add: /relocate + to: / + before: setup +imageSpec: + config: + env: + PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + LANG: "" + LC_ALL: POSIX + user: 64535 + diff --git a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go new file mode 100644 index 0000000..43800fc --- /dev/null +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -0,0 +1,97 @@ +/* +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 main + +import ( + "flag" + "os" + + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + _ "k8s.io/client-go/plugin/pkg/client/auth" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonrepository" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +var scheme = runtime.NewScheme() + +func init() { + _ = clientgoscheme.AddToScheme(scheme) + _ = helmv1alpha1.AddToScheme(scheme) + _ = sourcev1.AddToScheme(scheme) +} + +func main() { + var ( + metricsAddr string + healthProbeAddr string + enableLeaderElection bool + ) + + flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to.") + flag.StringVar(&healthProbeAddr, "health-probe-bind-address", ":9440", "The address the health probe endpoint binds to.") + flag.BoolVar(&enableLeaderElection, "leader-elect", false, "Enable leader election for controller manager.") + + // TODO: replace zap by deckhouse logger + + opts := zap.Options{Development: false} + opts.BindFlags(flag.CommandLine) + flag.Parse() + + ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + logger := ctrl.Log.WithName("setup") + + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: metricsAddr, + }, + HealthProbeBindAddress: healthProbeAddr, + LeaderElection: enableLeaderElection, + LeaderElectionID: "operator-helm-controller.helm.deckhouse.io", + }) + if err != nil { + logger.Error(err, "unable to create manager") + os.Exit(1) + } + + if err := helmclusteraddonrepository.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddonRepository controller") + os.Exit(1) + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + logger.Error(err, "unable to set up health check") + os.Exit(1) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + logger.Error(err, "unable to set up ready check") + os.Exit(1) + } + + logger.Info("starting manager") + if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil { + logger.Error(err, "manager exited with error") + os.Exit(1) + } +} diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod index cb18e58..4fefdc4 100644 --- a/images/operator-helm-artifact/go.mod +++ b/images/operator-helm-artifact/go.mod @@ -2,35 +2,71 @@ module github.com/deckhouse/operator-helm go 1.25.0 +replace github.com/deckhouse/operator-helm/api => ../../api + require ( + github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 + github.com/werf/nelm-source-controller/api v0.1.4 k8s.io/apimachinery v0.35.1 + k8s.io/client-go v0.35.1 k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 + sigs.k8s.io/controller-runtime v0.23.1 ) require ( - github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/zapr v1.3.0 // 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/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // 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 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect + github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 // indirect github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sync v0.19.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/time v0.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/images/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum index 98f6cbf..3fa068b 100644 --- a/images/operator-helm-artifact/go.sum +++ b/images/operator-helm-artifact/go.sum @@ -1,93 +1,79 @@ -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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= -github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 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-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= 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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= -github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= -github.com/spf13/pflag v1.0.9/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.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/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 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/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= +github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= -github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= -go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 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.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= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= 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-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= -k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= -sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= -sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= -sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go new file mode 100644 index 0000000..d037095 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -0,0 +1,58 @@ +/* +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 helmclusteraddonrepository + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddonrepository-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + // FinalizerName is the finalizer added to HelmClusterRepository to ensure cleanup. + FinalizerName = "helm.deckhouse.io/cleanup" + + // ConditionTypeReady is the condition type for readiness. + ConditionTypeReady = "Ready" + + // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. + ReasonMirrorSucceeded = "MirrorSucceeded" + + // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. + ReasonMirrorFailed = "MirrorFailed" + + // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. + ReasonInternalNotReady = "InternalNotReady" + + // ReasonInternalReady indicates the internal HelmRepository has reported Ready. + ReasonInternalReady = "InternalReady" + + // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. + ReasonCleanupFailed = "CleanupFailed" + + // LabelManagedBy marks an internal HelmRepository as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. + LabelManagedByValue = "operator-helm" + + // LabelSourceName stores the name of the source HelmClusterAddonRepository. + LabelSourceName = "helm.deckhouse.io/source-name" + + // DefaultInterval is the default reconciliation interval for the internal HelmRepository. + DefaultInterval = "10m" +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go new file mode 100644 index 0000000..438408d --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go @@ -0,0 +1,84 @@ +/* +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 helmclusteraddonrepository + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +// SetupWithManager registers the HelmClusterRepository controller with the manager. +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + // Primary watch: HelmClusterRepository (cluster-scoped). + For(&helmv1alpha1.HelmClusterAddonRepository{}). + // Secondary watch: HelmRepository in target namespace. + // When the internal resource changes (e.g., status update from + // nelm-source-controller), enqueue the parent HelmClusterRepository. + Watches( + &sourcev1.HelmRepository{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + ). + Complete(r) +} + +// mapInternalToCluster maps a HelmRepository event back to the +// HelmClusterRepository that owns it (by matching name and labels). +func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + // Only process resources in our target namespace. + if obj.GetNamespace() != TargetNamespace { + return nil + } + + // Only process resources managed by this controller. + labels := obj.GetLabels() + if labels[LabelManagedBy] != LabelManagedByValue { + return nil + } + + sourceName := labels[LabelSourceName] + if sourceName == "" { + logger.Info("Internal repository resource missing source-name label, skipping", + "name", obj.GetName(), "namespace", obj.GetNamespace()) + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + // HelmClusterAddonRepository is cluster-scoped, so no namespace. + Name: sourceName, + }, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go new file mode 100644 index 0000000..1166490 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go @@ -0,0 +1,147 @@ +/* +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 helmclusteraddonrepository + +import ( + "strings" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +// BuildDesiredHelmRepository constructs the desired state of the internal +// HelmRepository from the given HelmClusterAddonRepository. The returned object +// is not persisted — the caller is responsible for creating or patching. +func BuildDesiredHelmRepository(src *helmv1alpha1.HelmClusterAddonRepository) *sourcev1.HelmRepository { + repoType := sourcev1.HelmRepositoryTypeDefault + + // TODO: need to involve a factory to handle different schemas + if strings.HasPrefix(src.Spec.URL, "oci://") { + repoType = sourcev1.HelmRepositoryTypeOCI + } + + dst := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: src.Name, + Namespace: TargetNamespace, + Labels: map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: src.Name, + }, + }, + Spec: sourcev1.HelmRepositorySpec{ + URL: src.Spec.URL, + Type: repoType, + Interval: metav1.Duration{ + // TODO: remove magic number + Duration: 10 * time.Minute, + }, + }, + } + + // Map TLSVerify → Insecure (inverted semantics). + // Insecure is only meaningful for OCI repositories, but we set it + // consistently so the intent is clear. + if !src.Spec.TLSVerify { + dst.Spec.Insecure = true + } + + return dst +} + +// ApplyDesiredSpec updates an existing HelmRepository's spec fields to match +// the desired state. Returns true if any field was changed. +func ApplyDesiredSpec(existing *sourcev1.HelmRepository, desired *sourcev1.HelmRepository) bool { + changed := false + + if existing.Spec.URL != desired.Spec.URL { + existing.Spec.URL = desired.Spec.URL + changed = true + } + if existing.Spec.Type != desired.Spec.Type { + existing.Spec.Type = desired.Spec.Type + changed = true + } + if existing.Spec.Insecure != desired.Spec.Insecure { + existing.Spec.Insecure = desired.Spec.Insecure + changed = true + } + if existing.Spec.Interval != desired.Spec.Interval { + existing.Spec.Interval = desired.Spec.Interval + changed = true + } + + // Ensure labels are up to date. + if existing.Labels == nil { + existing.Labels = make(map[string]string) + } + for k, v := range desired.Labels { + if existing.Labels[k] != v { + existing.Labels[k] = v + changed = true + } + } + + return changed +} + +// TODO: need to re-work these statuses according to adr + +// MapInternalStatusToClusterConditions translates the internal HelmRepository +// status into conditions suitable for the HelmClusterRepository status. +func MapInternalStatusToClusterConditions(internal *sourcev1.HelmRepository) []metav1.Condition { + now := metav1.Now() + + // Find the Ready condition on the internal resource. + var readyCond *metav1.Condition + for i := range internal.Status.Conditions { + if internal.Status.Conditions[i].Type == ConditionTypeReady { + readyCond = &internal.Status.Conditions[i] + break + } + } + + if readyCond == nil { + return []metav1.Condition{ + { + Type: ConditionTypeReady, + Status: metav1.ConditionUnknown, + Reason: ReasonInternalNotReady, + Message: "Initializing repository..", + LastTransitionTime: now, + }, + } + } + + reason := ReasonInternalNotReady + if readyCond.Status == metav1.ConditionTrue { + reason = ReasonInternalReady + } + + return []metav1.Condition{ + { + Type: ConditionTypeReady, + Status: readyCond.Status, + Reason: reason, + Message: readyCond.Message, + LastTransitionTime: now, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go new file mode 100644 index 0000000..1cc1246 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -0,0 +1,175 @@ +/* +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 helmclusteraddonrepository + +import ( + "context" + "fmt" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +// Reconciler reconciles HelmClusterRepository objects by mirroring them +// to namespaced HelmRepository resources in the target namespace. +type Reconciler struct { + Client client.Client +} + +// Reconcile implements reconcile.Reconciler. +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusterrepository", req.Name) + + var clusterRepo helmv1alpha1.HelmClusterAddonRepository + if err := r.Client.Get(ctx, req.NamespacedName, &clusterRepo); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("HelmClusterAddonRepository not found, skipping") + return reconcile.Result{}, nil + } + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) + } + + if !clusterRepo.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &clusterRepo) + } + + if !controllerutil.ContainsFinalizer(&clusterRepo, FinalizerName) { + controllerutil.AddFinalizer(&clusterRepo, FinalizerName) + if err := r.Client.Update(ctx, &clusterRepo); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + } + + desired := BuildDesiredHelmRepository(&clusterRepo) + + var existing sourcev1.HelmRepository + err := r.Client.Get(ctx, types.NamespacedName{ + Name: desired.Name, + Namespace: desired.Namespace, + }, &existing) + + if apierrors.IsNotFound(err) { + logger.Info("Creating internal repository custom resource", "name", desired.Name, "namespace", desired.Namespace) + if err := r.Client.Create(ctx, desired); err != nil { + r.setCondition(&clusterRepo, metav1.ConditionFalse, ReasonMirrorFailed, + fmt.Sprintf("Failed to create internal custom resource: %v", err)) + _ = r.Client.Status().Update(ctx, &clusterRepo) + return reconcile.Result{}, fmt.Errorf("creating internal repository custom resource: %w", err) + } + // After creation, the internal resource has no status yet. + r.setCondition(&clusterRepo, metav1.ConditionUnknown, ReasonMirrorSucceeded, + "Internal repository custom resource created, waiting for status") + if err := r.Client.Status().Update(ctx, &clusterRepo); err != nil { + return reconcile.Result{}, fmt.Errorf("updating status after create: %w", err) + } + return reconcile.Result{}, nil + } + if err != nil { + return reconcile.Result{}, fmt.Errorf("getting internal repository custom resource: %w", err) + } + + if ApplyDesiredSpec(&existing, desired) { + logger.Info("Updating internal repository custom resource spec", "name", existing.Name) + if err := r.Client.Update(ctx, &existing); err != nil { + r.setCondition(&clusterRepo, metav1.ConditionFalse, ReasonMirrorFailed, + fmt.Sprintf("Failed to update internal repository custom resource: %v", err)) + _ = r.Client.Status().Update(ctx, &clusterRepo) + return reconcile.Result{}, fmt.Errorf("updating internal custom resource: %w", err) + } + } + + // 7. Propagate status from internal → cluster. + conditions := MapInternalStatusToClusterConditions(&existing) + clusterRepo.Status.Conditions = conditions + clusterRepo.Status.ObservedGeneration = clusterRepo.Generation + if err := r.Client.Status().Update(ctx, &clusterRepo); err != nil { + return reconcile.Result{}, fmt.Errorf("updating internal custom resource status: %w", err) + } + + return reconcile.Result{}, nil +} + +// reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. +func (r *Reconciler) reconcileDelete(ctx context.Context, clusterRepo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", clusterRepo.Name) + + if !controllerutil.ContainsFinalizer(clusterRepo, FinalizerName) { + return reconcile.Result{}, nil + } + + // Delete the internal repository resource. + var internal sourcev1.HelmRepository + err := r.Client.Get(ctx, types.NamespacedName{ + Name: clusterRepo.Name, + Namespace: TargetNamespace, + }, &internal) + + if err == nil { + logger.Info("Deleting internal repository resource", "name", internal.Name, "namespace", internal.Namespace) + if err := r.Client.Delete(ctx, &internal); err != nil && !apierrors.IsNotFound(err) { + r.setCondition(clusterRepo, metav1.ConditionFalse, ReasonCleanupFailed, + fmt.Sprintf("Failed to delete internal repository resource: %v", err)) + _ = r.Client.Status().Update(ctx, clusterRepo) + return reconcile.Result{}, fmt.Errorf("deleting internal repository resource: %w", err) + } + } else if !apierrors.IsNotFound(err) { + return reconcile.Result{}, fmt.Errorf("getting internal repository resource for deletion: %w", err) + } + + // Remove finalizer. + controllerutil.RemoveFinalizer(clusterRepo, FinalizerName) + if err := r.Client.Update(ctx, clusterRepo); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + return reconcile.Result{}, nil +} + +// setCondition is a helper to set a single Ready condition on the cluster resource. +func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + newCond := metav1.Condition{ + Type: ConditionTypeReady, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: now, + ObservedGeneration: repo.Generation, + } + + // Replace existing Ready condition or append. + for i, c := range repo.Status.Conditions { + if c.Type == ConditionTypeReady { + // Only update LastTransitionTime if status actually changed. + if c.Status == status { + newCond.LastTransitionTime = c.LastTransitionTime + } + repo.Status.Conditions[i] = newCond + return + } + } + repo.Status.Conditions = append(repo.Status.Conditions, newCond) +} diff --git a/images/operator-helm-artifact/werf.inc.yaml b/images/operator-helm-artifact/werf.inc.yaml new file mode 100644 index 0000000..165af3d --- /dev/null +++ b/images/operator-helm-artifact/werf.inc.yaml @@ -0,0 +1,57 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: +- add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +- add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - go.mod + - go.sum + setup: + - "**/*.go" +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +final: false +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} +import: +- image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /src + before: install +# TODO: uncomment as soon as CI will be ready +# secrets: +# - id: GOPROXY +# value: {{ .GOPROXY }} +# mount: +# - fromPath: ~/go-pkg-cache +# to: /go/pkg +shell: + install: + # TODO: uncomment as soon as CI will be ready + # - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /src/images/operator-helm-artifact + - go mod download + setup: + - cd /src/images/operator-helm-artifact + - mkdir /out + - export GOOS=linux + - export GOARCH=amd64 + - export CGO_ENABLED=0 + + - | + echo "Build operator-helm-controller binary" + {{- $_ := set $ "ProjectName" (list $.ImageName "operator-helm-controller" | join "/") }} + + {{- $buildCommand := printf "go build -ldflags=\"-s -w\" -tags %s -v -a -o /out/operator-helm-controller ./cmd/operator-helm-controller" .MODULE_EDITION -}} + {{- include "image-build.build" (set $ "BuildCommand" $buildCommand) | nindent 4 }} + diff --git a/images/operator-helm-controller/mount-points.yaml b/images/operator-helm-controller/mount-points.yaml new file mode 100644 index 0000000..eefff43 --- /dev/null +++ b/images/operator-helm-controller/mount-points.yaml @@ -0,0 +1 @@ +dirs: [] diff --git a/images/operator-helm-controller/werf.inc.yaml b/images/operator-helm-controller/werf.inc.yaml new file mode 100644 index 0000000..6656877 --- /dev/null +++ b/images/operator-helm-controller/werf.inc.yaml @@ -0,0 +1,16 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }} +fromImage: {{ .ModuleNamePrefix }}distroless +git: + {{- include "image mount points" . }} +import: +- image: {{ .ModuleNamePrefix }}operator-helm-artifact + add: /out/operator-helm-controller + to: /app/operator-helm-controller + after: install +imageSpec: + config: + user: 64535 + workingDir: "/app" + entrypoint: ["/app/operator-helm-controller"] + diff --git a/templates/operator-helm-controller/deployment.yaml b/templates/operator-helm-controller/deployment.yaml new file mode 100644 index 0000000..64e6e35 --- /dev/null +++ b/templates/operator-helm-controller/deployment.yaml @@ -0,0 +1,133 @@ +{{- $priorityClassName := include "priorityClassName" . }} + +{{- define "operator_helm_controller_resources" }} +cpu: 50m +memory: 64Mi +{{- end }} + +{{- if (.Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" "workload-resource-policy.deckhouse.io" "master")) | nindent 2 }} +spec: + targetRef: + apiVersion: "apps/v1" + kind: Deployment + name: operator-helm-controller + updatePolicy: + updateMode: {{ include "vpa.policyUpdateMode" . }} + resourcePolicy: + containerPolicies: + {{- include "kube_api_rewriter.vpa_container_policy" . | nindent 4 }} + {{- include "kube_rbac_proxy.vpa_container_policy" . | nindent 4 }} + - containerName: operator-helm-controller + minAllowed: + {{- include "operator_helm_controller_resources" . | nindent 8 }} + maxAllowed: + cpu: 1000m + memory: 1Gi +{{- end }} + +--- +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" )) | nindent 2 }} +spec: + minAvailable: {{ include "helm_lib_is_ha_to_value" (list . 1 0) }} + selector: + matchLabels: + app: operator-helm-controller + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + replicas: {{ include "helm_lib_is_ha_to_value" (list . 3 1) }} + {{- if (include "helm_lib_ha_enabled" .) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 0 + maxUnavailable: 1 + {{- end }} + revisionHistoryLimit: 2 + selector: + matchLabels: + app: operator-helm-controller + template: + metadata: + labels: + app: operator-helm-controller + annotations: + kubectl.kubernetes.io/default-container: operator-helm-controller + spec: + containers: + {{- include "kube_api_rewriter.sidecar_container" . | nindent 8 }} + - name: operator-helm-controller + {{- include "helm_lib_module_container_security_context_read_only_root_filesystem_capabilities_drop_all_pss_restricted" . | nindent 10 }} + image: {{ include "helm_lib_module_image" (list . "operatorHelmController") }} + imagePullPolicy: IfNotPresent + args: + {{/* TODO: add log level option */}} + - --leader-elect + - --metrics-bind-address=:8080 + - --health-probe-bind-address=:9440 + volumeMounts: + {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} + ports: + - containerPort: 8080 + name: metrics + protocol: TCP + - containerPort: 9440 + name: healthz + protocol: TCP + resources: + requests: + {{- include "helm_lib_module_ephemeral_storage_only_logs" . | nindent 14 }} + {{- if not ( .Values.global.enabledModules | has "vertical-pod-autoscaler-crd") }} + {{- include "operator_helm_controller_resources" . | nindent 14 }} + {{- end }} + env: + {{- include "kube_api_rewriter.kubeconfig_env" . | nindent 12 }} + livenessProbe: + httpGet: + path: /healthz + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + readinessProbe: + httpGet: + path: / + port: healthz + scheme: HTTP + initialDelaySeconds: 10 + {{- $kubeRbacProxySettings := dict }} + {{- $_ := set $kubeRbacProxySettings "runAsUserNobody" false }} + {{- $_ := set $kubeRbacProxySettings "ignorePaths" "/proxy/healthz,/proxy/readyz" }} + {{- $_ := set $kubeRbacProxySettings "upstreams" (list + (dict "upstream" "http://127.0.0.1:8080/metrics" "path" "/metrics" "name" "operator-helm-controller") + (dict "upstream" "http://127.0.0.1:9090/metrics" "path" "/proxy/metrics" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/healthz" "path" "/proxy/healthz" "name" "kube-api-rewriter") + (dict "upstream" "http://127.0.0.1:9090/readyz" "path" "/proxy/readyz" "name" "kube-api-rewriter") + ) }} + {{- include "kube_rbac_proxy.sidecar_container" (tuple . $kubeRbacProxySettings) | nindent 8 }} + dnsPolicy: ClusterFirst + serviceAccountName: operator-helm-controller + {{- include "helm_lib_module_pod_security_context_run_as_user_deckhouse" . | nindent 6 }} + {{- include "helm_lib_priority_class" (tuple . $priorityClassName) | nindent 6 }} + {{- include "helm_lib_node_selector" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} + {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "operator-helm-controller")) | nindent 6 }} + volumes: + {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/operator-helm-controller/rbac-for-us.yaml b/templates/operator-helm-controller/rbac-for-us.yaml new file mode 100644 index 0000000..7bc6f85 --- /dev/null +++ b/templates/operator-helm-controller/rbac-for-us.yaml @@ -0,0 +1,216 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +imagePullSecrets: +- name: operator-helm-module-registry +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: d8:{{ .Chart.Name }}:operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: d8:{{ .Chart.Name }}:operator-helm-controller +rules: +- apiGroups: + - "" + resources: + - pods + - services + - secrets + - configmaps + verbs: + - get + - create + - update + - delete + - list + - watch + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - "" + resources: + - secrets + - serviceaccounts + verbs: + - get + - list + - watch +- apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create +- apiGroups: + - helm.deckhouse.io + resources: + - helmclusteraddons + - helmclusteraddons/status + - helmclusteraddoncharts + - helmclusteraddoncharts/status + - helmclusteraddonrepositories + - helmclusteraddonrepositories/status + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - helm.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorhelmreleases/status + verbs: + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets + - internalnelmoperatorgitrepositories + - internalnelmoperatorhelmcharts + - internalnelmoperatorhelmrepositories + - internalnelmoperatorocirepositories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/finalizers + - internalnelmoperatorgitrepositories/finalizers + - internalnelmoperatorhelmcharts/finalizers + - internalnelmoperatorhelmrepositories/finalizers + - internalnelmoperatorocirepositories/finalizers + verbs: + - create + - delete + - get + - patch + - update +- apiGroups: + - source.internal.operator-helm.deckhouse.io + resources: + - internalnelmoperatorbuckets/status + - internalnelmoperatorgitrepositories/status + - internalnelmoperatorhelmcharts/status + - internalnelmoperatorhelmrepositories/status + - internalnelmoperatorocirepositories/status + verbs: + - get + - patch + - update +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +rules: +- apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +- apiGroups: + - "" + resources: + - configmaps/status + verbs: + - get + - update + - patch +- apiGroups: + - "" + resources: + - events + verbs: + - create +- apiGroups: + - "coordination.k8s.io" + resources: + - leases + verbs: + - get + - list + - watch + - create + - update + - patch + - delete +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: operator-helm-controller +subjects: +- kind: ServiceAccount + name: operator-helm-controller diff --git a/templates/operator-helm-controller/service-metrics.yaml b/templates/operator-helm-controller/service-metrics.yaml new file mode 100644 index 0000000..89b9345 --- /dev/null +++ b/templates/operator-helm-controller/service-metrics.yaml @@ -0,0 +1,16 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: operator-helm-controller-metrics + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + ports: + - name: metrics + port: 8080 + protocol: TCP + targetPort: https-metrics + selector: + app: operator-helm-controller + diff --git a/templates/operator-helm-controller/service-monitor.yaml b/templates/operator-helm-controller/service-monitor.yaml new file mode 100644 index 0000000..a17f1db --- /dev/null +++ b/templates/operator-helm-controller/service-monitor.yaml @@ -0,0 +1,24 @@ +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: operator-helm-controller + namespace: d8-monitoring + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller" "prometheus" "main")) | nindent 2 }} +spec: + endpoints: + - bearerTokenSecret: + key: token + name: prometheus-token + path: /metrics + port: metrics + scheme: https + tlsConfig: + insecureSkipVerify: true + namespaceSelector: + matchNames: + - d8-{{ .Chart.Name }} + selector: + matchLabels: + app: "operator-helm-controller" + diff --git a/templates/rbac-to-us.yaml b/templates/rbac-to-us.yaml index a7e15af..ed3697f 100644 --- a/templates/rbac-to-us.yaml +++ b/templates/rbac-to-us.yaml @@ -8,7 +8,7 @@ metadata: rules: - apiGroups: ["apps"] resources: ["deployments/prometheus-metrics"] - resourceNames: ["helm-controller", "nelm-source-controller", "kube-api-rewriter"] + resourceNames: ["operator-helm-controller", "helm-controller", "nelm-source-controller", "kube-api-rewriter"] verbs: ["get"] {{- if (.Values.global.enabledModules | has "prometheus") }} From 3fd32e64cb499bdcad1ef95e617b30cd4b1c886d Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 25 Feb 2026 22:43:28 +0300 Subject: [PATCH 06/26] fix: correct operator-helm crds Signed-off-by: Ilya Drey --- api/v1alpha1/helm_cluster_addon.go | 3 +-- api/v1alpha1/helm_cluster_addon_chart.go | 1 - api/v1alpha1/helm_cluster_addon_repository.go | 3 +-- crds/helmclusteraddoncharts.yaml | 2 -- crds/helmclusteraddonrepositories.yaml | 2 -- crds/helmclusteraddons.yaml | 2 -- 6 files changed, 2 insertions(+), 11 deletions(-) diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index e59b818..b1700df 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -42,8 +42,7 @@ type HelmClusterAddon struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec HelmClusterAddonSpec `json:"spec"` - // +kubebuilder:default:={"observedGeneration":-1} + Spec HelmClusterAddonSpec `json:"spec"` Status HelmClusterAddonStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/helm_cluster_addon_chart.go b/api/v1alpha1/helm_cluster_addon_chart.go index 9a87e82..3ac8d7c 100644 --- a/api/v1alpha1/helm_cluster_addon_chart.go +++ b/api/v1alpha1/helm_cluster_addon_chart.go @@ -38,7 +38,6 @@ type HelmClusterAddonChart struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - // +kubebuilder:default:={"observedGeneration":-1} Status HelmClusterAddonChartStatus `json:"status,omitempty"` } diff --git a/api/v1alpha1/helm_cluster_addon_repository.go b/api/v1alpha1/helm_cluster_addon_repository.go index 44d37cb..2ff5669 100644 --- a/api/v1alpha1/helm_cluster_addon_repository.go +++ b/api/v1alpha1/helm_cluster_addon_repository.go @@ -39,8 +39,7 @@ type HelmClusterAddonRepository struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec HelmClusterAddonRepositorySpec `json:"spec"` - // +kubebuilder:default:={"observedGeneration":-1} + Spec HelmClusterAddonRepositorySpec `json:"spec"` Status HelmClusterAddonRepositoryStatus `json:"status,omitempty"` } diff --git a/crds/helmclusteraddoncharts.yaml b/crds/helmclusteraddoncharts.yaml index cf37e81..e776b53 100644 --- a/crds/helmclusteraddoncharts.yaml +++ b/crds/helmclusteraddoncharts.yaml @@ -43,8 +43,6 @@ spec: metadata: type: object status: - default: - observedGeneration: -1 properties: conditions: description: Conditions represent the latest available observations diff --git a/crds/helmclusteraddonrepositories.yaml b/crds/helmclusteraddonrepositories.yaml index d69a87c..30c866b 100644 --- a/crds/helmclusteraddonrepositories.yaml +++ b/crds/helmclusteraddonrepositories.yaml @@ -70,8 +70,6 @@ spec: - url type: object status: - default: - observedGeneration: -1 properties: conditions: description: Conditions represent the latest available observations diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml index 8f92b6f..aecb1e5 100644 --- a/crds/helmclusteraddons.yaml +++ b/crds/helmclusteraddons.yaml @@ -86,8 +86,6 @@ spec: - chart type: object status: - default: - observedGeneration: -1 properties: conditions: description: Conditions represent the latest available observations From 55c86c59e4cab78c743c438306fe2ace825de8b4 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 25 Feb 2026 22:58:29 +0300 Subject: [PATCH 07/26] fix: correct operator-helm templates Signed-off-by: Ilya Drey --- templates/operator-helm-controller/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/operator-helm-controller/deployment.yaml b/templates/operator-helm-controller/deployment.yaml index 64e6e35..6cfad87 100644 --- a/templates/operator-helm-controller/deployment.yaml +++ b/templates/operator-helm-controller/deployment.yaml @@ -108,7 +108,7 @@ spec: initialDelaySeconds: 10 readinessProbe: httpGet: - path: / + path: /readyz port: healthz scheme: HTTP initialDelaySeconds: 10 From 7b23e0eb93845cced7161be7623d2b2f18b682ba Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 26 Feb 2026 11:37:42 +0300 Subject: [PATCH 08/26] chore: update codegen scripts Signed-off-by: Ilya Drey --- api/scripts/update-codegen.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/api/scripts/update-codegen.sh b/api/scripts/update-codegen.sh index 0d60587..544695f 100755 --- a/api/scripts/update-codegen.sh +++ b/api/scripts/update-codegen.sh @@ -26,6 +26,8 @@ EOF } function source::settings { + echo "Preparing variables and sourcing dependency scripts.." + SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" API_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd -P)" ROOT="$(cd "${API_ROOT}/.." && pwd -P)" @@ -33,9 +35,13 @@ function source::settings { THIS_PKG="github.com/deckhouse/operator-helm/api" source "${CODEGEN_PKG}/kube_codegen.sh" + + echo "Completed!" } function generate::v1alpha1 { + echo "Generating v1alpha1 soruces.." + kube::codegen::gen_helpers \ --boilerplate "${SCRIPT_DIR}/boilerplate.go.txt" \ "${API_ROOT}/v1alpha1" @@ -54,9 +60,13 @@ function generate::v1alpha1 { --output-pkg "${THIS_PKG}/client/generated" \ --boilerplate "${SCRIPT_DIR}/boilerplate.go.txt" \ "${ROOT}" + + echo "Completed!" } function generate::crds { + echo "Generating CRDs.." + OUTPUT_BASE=$(mktemp -d) trap 'rm -rf "${OUTPUT_BASE}"' ERR EXIT @@ -66,6 +76,8 @@ function generate::crds { for file in $(find "${OUTPUT_BASE}"/* -type f -iname "*.yaml"); do cp "$file" "${ROOT}/crds/$(echo $file | awk -Fio_ '{print $2}')" done + + echo "Completed!" } WHAT=$1 From a1bd5225e59ec6d3cc02e6281f432c6bfd09f765 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 26 Feb 2026 11:38:14 +0300 Subject: [PATCH 09/26] feat: update operator-helm apis Signed-off-by: Ilya Drey --- api/Taskfile.dist.yaml | 5 +- api/v1alpha1/helm_cluster_addon.go | 22 +- api/v1alpha1/helm_cluster_addon_chart.go | 17 +- api/v1alpha1/helm_cluster_addon_repository.go | 11 +- crds/helmclusteraddoncharts.yaml | 211 ++++++------- crds/helmclusteraddonrepositories.yaml | 258 +++++++++------- crds/helmclusteraddons.yaml | 287 ++++++++++-------- .../pkg/api/openapi/zz_generated.openapi.go | 86 ++++-- 8 files changed, 504 insertions(+), 393 deletions(-) diff --git a/api/Taskfile.dist.yaml b/api/Taskfile.dist.yaml index 11ed1c9..f58ee84 100644 --- a/api/Taskfile.dist.yaml +++ b/api/Taskfile.dist.yaml @@ -28,10 +28,11 @@ tasks: format:yaml: desc: "Format non-templated YAML files, e.g. CRDs" cmds: + # TODO: replace prettier image - | cd ../ && docker run --rm \ - -v ./:/tmp/virt ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ - sh -c "cd /tmp/virt ; prettier -w \"crds/*.yaml\"" + -v "$(pwd):/tmp/operator-helm" ghcr.io/deckhouse/virtualization/prettier:3.2.5 \ + sh -c "cd /tmp/operator-helm ; prettier -w \"crds/*.yaml\"" _ci:verify-gen: desc: "Check generated files are up-to-date." diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index b1700df..0198456 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -48,22 +48,38 @@ type HelmClusterAddon struct { type HelmClusterAddonSpec struct { Chart HelmClusterAddonChartRef `json:"chart"` - // Values holds the values for this Helm release. + // Values holds the values for this HelmClusterAddon release. // +kubebuilder:pruning:PreserveUnknownFields // +optional Values *apiextensionsv1.JSON `json:"values"` + // Namespace to deploy cluster addon release // +kubebuilder:default:="default" // +optional + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=63 Namespace string `json:"namespace"` + // Maintenance specifies the reconciliation strategy for the resource. + // When set to "NoResourceReconciliation", the controller will stop updating the + // underlying resources, allowing for manual intervention or maintenance + // without the operator overwriting changes. + // When empty (""), standard reconciliation is active. // +kubebuilder:validation:Enum="";NoResourceReconciliation // +optional Maintanace string `json:"maintanace,omitempty"` } type HelmClusterAddonChartRef struct { + // Specifies the name of the Helm chart to be installed + // from the defined repository (e.g., "ingress-nginx" or "redis"). + // +kubebuilder:validation:MinLength=1 + HelmClusterAddonChartName string `json:"helmClusterAddonChart"` + // Specifies the name of the HelmClusterAddonRepository custom resource that contains + // the connection details and credentials for the repository where + // the chart is located. + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=63 HelmClusterAddonRepository string `json:"helmClusterAddonRepository"` - HelmClusterAddonChartName string `json:"helmClusterAddonChart"` - // Versions holds the Chart version. + // Versions holds the HelmClusterAddon chart version. // +optional Version string `json:"version,omitempty"` } diff --git a/api/v1alpha1/helm_cluster_addon_chart.go b/api/v1alpha1/helm_cluster_addon_chart.go index 3ac8d7c..5ed8b0b 100644 --- a/api/v1alpha1/helm_cluster_addon_chart.go +++ b/api/v1alpha1/helm_cluster_addon_chart.go @@ -42,7 +42,12 @@ type HelmClusterAddonChart struct { } type HelmClusterAddonChartSpec struct { - ChartName string `json:"chartName"` + // Helm chart name + // +kubebuilder:validation:MinLength=1 + ChartName string `json:"chartName"` + // Name of HelmClusterAddonRepository where respective helm chart resides. + // +kubebuilder:validation:MinLength=3 + // +kubebuilder:validation:MaxLength=63 RepositoryName string `json:"repositoryName"` } @@ -50,16 +55,18 @@ type HelmClusterAddonChartStatus struct { // Conditions represent the latest available observations of the repository state. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` + // Available helm chart versions // +optional Versions []HelmClusterAddonChartVersion `json:"versions"` } -// TODO: need to clarify what kind of information we need to render in UI for every available chart version. -// It makes sense to create Internal chart only during the first application deploy. - type HelmClusterAddonChartVersion struct { + // Helm chart version + // +kubebuilder:validation:MinLength=1 Version string `json:"version"` - Digest string `json:"digest"` + // Helm chart digest + // +kubebuilder:validation:MinLength=1 + Digest string `json:"digest"` } // HelmClusterAddonChartList contains a list of HelmClusterAddonCharts. diff --git a/api/v1alpha1/helm_cluster_addon_repository.go b/api/v1alpha1/helm_cluster_addon_repository.go index 2ff5669..000f88f 100644 --- a/api/v1alpha1/helm_cluster_addon_repository.go +++ b/api/v1alpha1/helm_cluster_addon_repository.go @@ -46,7 +46,7 @@ type HelmClusterAddonRepository struct { type HelmClusterAddonRepositorySpec struct { // URL of the Helm repository. Supports http(s):// and oci:// protocols. // +kubebuilder:validation:Required - // +kubebuilder:validation:Pattern=`^(https?|oci)://.*$` + // +kubebuilder:validation:XValidation:rule="self.matches('^(https?|oci)://.+$')",message="URL must have a valid protocol (http, https, oci) and a non-empty path" URL string `json:"url"` // Auth contains authentication credentials for the repository. @@ -65,7 +65,14 @@ type HelmClusterAddonRepositorySpec struct { // TODO: define authentication requirements depeding on registry type -type HelmClusterAddonRepositoryAuth struct{} +type HelmClusterAddonRepositoryAuth struct { + // Repository authentication username. + // +kubebuilder:validation:MinLength=1 + Username string `json:"username"` + // Repository authentication password. + // +kubebuilder:validation:MinLength=1 + Password string `json:"password"` +} type HelmClusterAddonRepositoryStatus struct { // Conditions represent the latest available observations of the repository state. diff --git a/crds/helmclusteraddoncharts.yaml b/crds/helmclusteraddoncharts.yaml index e776b53..ba6d15c 100644 --- a/crds/helmclusteraddoncharts.yaml +++ b/crds/helmclusteraddoncharts.yaml @@ -12,111 +12,118 @@ spec: group: helm.deckhouse.io names: categories: - - all - - operator-helm + - all + - operator-helm kind: HelmClusterAddonChart listKind: HelmClusterAddonChartList plural: helmclusteraddoncharts singular: helmclusteraddonchart scope: Cluster versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: HelmClusterAddonChart represents a Helm chart from specific repository. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - status: - properties: - conditions: - description: Conditions represent the latest available observations - of the repository state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. - properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - versions: - items: - properties: - digest: - type: string - version: - type: string - required: - - digest - - version - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} + - name: v1alpha1 + schema: + openAPIV3Schema: + description: HelmClusterAddonChart represents a Helm chart from specific repository. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the repository state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + versions: + description: Available helm chart versions + items: + properties: + digest: + description: Helm chart digest + minLength: 1 + type: string + version: + description: Helm chart version + minLength: 1 + type: string + required: + - digest + - version + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/helmclusteraddonrepositories.yaml b/crds/helmclusteraddonrepositories.yaml index 30c866b..da1680a 100644 --- a/crds/helmclusteraddonrepositories.yaml +++ b/crds/helmclusteraddonrepositories.yaml @@ -12,133 +12,155 @@ spec: group: helm.deckhouse.io names: categories: - - all - - operator-helm + - all + - operator-helm kind: HelmClusterAddonRepository listKind: HelmClusterAddonRepositoryList plural: helmclusteraddonrepositories singular: helmclusteraddonrepository scope: Cluster versions: - - additionalPrinterColumns: - - description: The readiness status of the repository - jsonPath: .status.conditions[?(@.type=='Ready')].status - name: Status - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: HelmClusterAddonRepository represens a Git, Helm or OCI complient - repocitory with Helm charts. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - auth: - description: Auth contains authentication credentials for the repository. - type: object - caCertificate: - description: CACertificate is the PEM encoded CA certificate for TLS - verification. - type: string - tlsVerify: - default: true - description: TLSVerify enables or disables TLS certificate verification. - type: boolean - url: - description: URL of the Helm repository. Supports http(s):// and oci:// - protocols. - pattern: ^(https?|oci)://.*$ - type: string - required: - - url - type: object - status: - properties: - conditions: - description: Conditions represent the latest available observations - of the repository state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. + - additionalPrinterColumns: + - description: The readiness status of the repository + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddonRepository represens a Git, Helm or OCI complient + repocitory with Helm charts. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + auth: + description: Auth contains authentication credentials for the repository. properties: - lastTransitionTime: - description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 + password: + description: Repository authentication password. minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + username: + description: Repository authentication username. + minLength: 1 type: string required: - - lastTransitionTime - - message - - reason - - status - - type + - password + - username type: object - type: array - observedGeneration: - description: Generating a resource that was last processed by the - controller. - format: int64 - type: integer - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} + caCertificate: + description: + CACertificate is the PEM encoded CA certificate for TLS + verification. + type: string + tlsVerify: + default: true + description: TLSVerify enables or disables TLS certificate verification. + type: boolean + url: + description: + URL of the Helm repository. Supports http(s):// and oci:// + protocols. + type: string + x-kubernetes-validations: + - message: + URL must have a valid protocol (http, https, oci) and a + non-empty path + rule: self.matches('^(https?|oci)://.+$') + required: + - url + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the repository state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: + Generating a resource that was last processed by the + controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml index aecb1e5..7fcec2e 100644 --- a/crds/helmclusteraddons.yaml +++ b/crds/helmclusteraddons.yaml @@ -12,149 +12,172 @@ spec: group: helm.deckhouse.io names: categories: - - all - - operator-helm + - all + - operator-helm kind: HelmClusterAddon listKind: HelmClusterAddonList plural: helmclusteraddons singular: helmclusteraddon scope: Cluster versions: - - additionalPrinterColumns: - - description: Helm release chart name. - jsonPath: .spec.chart.helmClusterAddonChart - name: Chart Name - type: string - - description: Helm release chart version. - jsonPath: .spec.chart.version - name: Chart Version - type: string - - description: The readiness status of the repository - jsonPath: .status.conditions[?(@.type=='Ready')].status - name: Status - type: string - name: v1alpha1 - schema: - openAPIV3Schema: - description: HelmClusterAddon represents a Helm addon that is installed across - the whole cluster. - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - properties: - chart: - properties: - helmClusterAddonChart: - type: string - helmClusterAddonRepository: - type: string - version: - description: Versions holds the Chart version. - type: string - required: - - helmClusterAddonChart - - helmClusterAddonRepository - type: object - maintanace: - enum: - - "" - - NoResourceReconciliation - type: string - namespace: - default: default - type: string - values: - description: Values holds the values for this Helm release. - x-kubernetes-preserve-unknown-fields: true - required: - - chart - type: object - status: - properties: - conditions: - description: Conditions represent the latest available observations - of the repository state. - items: - description: Condition contains details for one aspect of the current - state of this API Resource. + - additionalPrinterColumns: + - description: Helm release chart name. + jsonPath: .spec.chart.helmClusterAddonChart + name: Chart Name + type: string + - description: Helm release chart version. + jsonPath: .spec.chart.version + name: Chart Version + type: string + - description: The readiness status of the repository + jsonPath: .status.conditions[?(@.type=='Ready')].status + name: Status + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: + HelmClusterAddon represents a Helm addon that is installed across + the whole cluster. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + chart: properties: - lastTransitionTime: + helmClusterAddonChart: description: |- - lastTransitionTime is the last time the condition transitioned from one status to another. - This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: |- - message is a human readable message indicating details about the transition. - This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: |- - observedGeneration represents the .metadata.generation that the condition was set based upon. - For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date - with respect to the current state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: |- - reason contains a programmatic identifier indicating the reason for the condition's last transition. - Producers of specific condition types may define expected values and meanings for this field, - and whether the values are considered a guaranteed API. - The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 + Specifies the name of the Helm chart to be installed + from the defined repository (e.g., "ingress-nginx" or "redis"). minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown + helmClusterAddonRepository: + description: |- + Specifies the name of the HelmClusterAddonRepository custom resource that contains + the connection details and credentials for the repository where + the chart is located. + maxLength: 63 + minLength: 3 type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + version: + description: Versions holds the HelmClusterAddon chart version. type: string required: - - lastTransitionTime - - message - - reason - - status - - type + - helmClusterAddonChart + - helmClusterAddonRepository type: object - type: array - observedGeneration: - description: Generating a resource that was last processed by the - controller. - format: int64 - type: integer - type: object - required: - - spec - type: object - served: true - storage: true - subresources: - status: {} + maintanace: + description: |- + Maintenance specifies the reconciliation strategy for the resource. + When set to "NoResourceReconciliation", the controller will stop updating the + underlying resources, allowing for manual intervention or maintenance + without the operator overwriting changes. + When empty (""), standard reconciliation is active. + enum: + - "" + - NoResourceReconciliation + type: string + namespace: + default: default + description: Namespace to deploy cluster addon release + maxLength: 63 + minLength: 3 + type: string + values: + description: Values holds the values for this HelmClusterAddon release. + x-kubernetes-preserve-unknown-fields: true + required: + - chart + type: object + status: + properties: + conditions: + description: + Conditions represent the latest available observations + of the repository state. + items: + description: + Condition contains details for one aspect of the current + state of this API Resource. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from one status to another. + This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: |- + message is a human readable message indicating details about the transition. + This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: |- + observedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: |- + reason contains a programmatic identifier indicating the reason for the condition's last transition. + Producers of specific condition types may define expected values and meanings for this field, + and whether the values are considered a guaranteed API. + The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: + Generating a resource that was last processed by the + controller. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index e1148ba..8ea159b 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -243,29 +243,31 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref co SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ - "helmClusterAddonRepository": { + "helmClusterAddonChart": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Specifies the name of the Helm chart to be installed from the defined repository (e.g., \"ingress-nginx\" or \"redis\").", + Default: "", + Type: []string{"string"}, + Format: "", }, }, - "helmClusterAddonChart": { + "helmClusterAddonRepository": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Specifies the name of the HelmClusterAddonRepository custom resource that contains the connection details and credentials for the repository where the chart is located.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "version": { SchemaProps: spec.SchemaProps{ - Description: "Versions holds the Chart version.", + Description: "Versions holds the HelmClusterAddon chart version.", Type: []string{"string"}, Format: "", }, }, }, - Required: []string{"helmClusterAddonRepository", "helmClusterAddonChart"}, + Required: []string{"helmClusterAddonChart", "helmClusterAddonRepository"}, }, }, } @@ -279,16 +281,18 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref c Properties: map[string]spec.Schema{ "chartName": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Helm chart name", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "repositoryName": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Name of HelmClusterAddonRepository where respective helm chart resides.", + Default: "", + Type: []string{"string"}, + Format: "", }, }, }, @@ -320,7 +324,8 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref }, "versions": { SchemaProps: spec.SchemaProps{ - Type: []string{"array"}, + Description: "Available helm chart versions", + Type: []string{"array"}, Items: &spec.SchemaOrArray{ Schema: &spec.Schema{ SchemaProps: spec.SchemaProps{ @@ -347,16 +352,18 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(re Properties: map[string]spec.Schema{ "version": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Helm chart version", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "digest": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Helm chart digest", + Default: "", + Type: []string{"string"}, + Format: "", }, }, }, @@ -469,6 +476,25 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth( Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "username": { + SchemaProps: spec.SchemaProps{ + Description: "Repository authentication username.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + "password": { + SchemaProps: spec.SchemaProps{ + Description: "Repository authentication password.", + Default: "", + Type: []string{"string"}, + Format: "", + }, + }, + }, + Required: []string{"username", "password"}, }, }, } @@ -616,21 +642,23 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref common }, "values": { SchemaProps: spec.SchemaProps{ - Description: "Values holds the values for this Helm release.", + Description: "Values holds the values for this HelmClusterAddon release.", Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), }, }, "namespace": { SchemaProps: spec.SchemaProps{ - Default: "", - Type: []string{"string"}, - Format: "", + Description: "Namespace to deploy cluster addon release", + Default: "", + Type: []string{"string"}, + Format: "", }, }, "maintanace": { SchemaProps: spec.SchemaProps{ - Type: []string{"string"}, - Format: "", + Description: "Maintenance specifies the reconciliation strategy for the resource. When set to \"NoResourceReconciliation\", the controller will stop updating the underlying resources, allowing for manual intervention or maintenance without the operator overwriting changes. When empty (\"\"), standard reconciliation is active.", + Type: []string{"string"}, + Format: "", }, }, }, From 24064449b9ccc4a178c02a661cd39a98079e0d20 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Sun, 1 Mar 2026 21:01:13 +0300 Subject: [PATCH 10/26] feat: update operator-helm HelmClusterAddonRepository logic Signed-off-by: Ilya Drey --- .../pkg/operatornelm/operatornelm_rules.go | 2 +- .../helmclusteraddonrepository/constants.go | 13 +- .../helmclusteraddonrepository/controller.go | 28 +- .../helmclusteraddonrepository/mapper.go | 97 +---- .../helmclusteraddonrepository/reconciler.go | 357 ++++++++++++++---- .../helmclusteraddonrepository/utils.go | 57 +++ 6 files changed, 386 insertions(+), 168 deletions(-) create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go diff --git a/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go index dd85d8d..c527d40 100644 --- a/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go +++ b/images/kube-api-rewriter/pkg/operatornelm/operatornelm_rules.go @@ -146,7 +146,7 @@ var OperatorNelmAPIGroupsRules = map[string]APIGroupRule{ ListKind: "HelmReleaseList", Plural: "helmreleases", Singular: "helmrelease", - Versions: []string{"v2beta2", "v2"}, + Versions: []string{"v2beta1", "v2beta2", "v2"}, PreferredVersion: "v2", Categories: []string{}, ShortNames: []string{"hr"}, diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index d037095..2129c74 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -16,6 +16,15 @@ limitations under the License. package helmclusteraddonrepository +import "time" + +type InternalRepositoryType string + +const ( + InternalHelmRepository InternalRepositoryType = "helm" + InternalOCIRepository InternalRepositoryType = "oci" +) + const ( // ControllerName is the name of this controller, used for leader election and logging. ControllerName = "helmclusteraddonrepository-controller" @@ -53,6 +62,6 @@ const ( // LabelSourceName stores the name of the source HelmClusterAddonRepository. LabelSourceName = "helm.deckhouse.io/source-name" - // DefaultInterval is the default reconciliation interval for the internal HelmRepository. - DefaultInterval = "10m" + // DefaultInterval is the default reconciliation interval for the internal repository. + DefaultInterval = 5 * time.Second ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go index 438408d..5403ed2 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go @@ -19,18 +19,20 @@ package helmclusteraddonrepository import ( "context" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) -// SetupWithManager registers the HelmClusterRepository controller with the manager. func SetupWithManager(mgr ctrl.Manager) error { r := &Reconciler{ Client: mgr.GetClient(), @@ -38,29 +40,32 @@ func SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). Named(ControllerName). - // Primary watch: HelmClusterRepository (cluster-scoped). For(&helmv1alpha1.HelmClusterAddonRepository{}). - // Secondary watch: HelmRepository in target namespace. - // When the internal resource changes (e.g., status update from - // nelm-source-controller), enqueue the parent HelmClusterRepository. Watches( &sourcev1.HelmRepository{}, handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &sourcev1.OCIRepository{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &corev1.Secret{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Complete(r) } -// mapInternalToCluster maps a HelmRepository event back to the -// HelmClusterRepository that owns it (by matching name and labels). func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { logger := log.FromContext(ctx) - // Only process resources in our target namespace. if obj.GetNamespace() != TargetNamespace { return nil } - // Only process resources managed by this controller. labels := obj.GetLabels() if labels[LabelManagedBy] != LabelManagedByValue { return nil @@ -70,14 +75,15 @@ func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Re if sourceName == "" { logger.Info("Internal repository resource missing source-name label, skipping", "name", obj.GetName(), "namespace", obj.GetNamespace()) + return nil } return []reconcile.Request{ { NamespacedName: types.NamespacedName{ - // HelmClusterAddonRepository is cluster-scoped, so no namespace. - Name: sourceName, + Name: sourceName, + Namespace: "", }, }, } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go index 1166490..cc9b384 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go @@ -17,103 +17,20 @@ limitations under the License. package helmclusteraddonrepository import ( - "strings" - "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) -// BuildDesiredHelmRepository constructs the desired state of the internal -// HelmRepository from the given HelmClusterAddonRepository. The returned object -// is not persisted — the caller is responsible for creating or patching. -func BuildDesiredHelmRepository(src *helmv1alpha1.HelmClusterAddonRepository) *sourcev1.HelmRepository { - repoType := sourcev1.HelmRepositoryTypeDefault - - // TODO: need to involve a factory to handle different schemas - if strings.HasPrefix(src.Spec.URL, "oci://") { - repoType = sourcev1.HelmRepositoryTypeOCI - } - - dst := &sourcev1.HelmRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: src.Name, - Namespace: TargetNamespace, - Labels: map[string]string{ - LabelManagedBy: LabelManagedByValue, - LabelSourceName: src.Name, - }, - }, - Spec: sourcev1.HelmRepositorySpec{ - URL: src.Spec.URL, - Type: repoType, - Interval: metav1.Duration{ - // TODO: remove magic number - Duration: 10 * time.Minute, - }, - }, - } - - // Map TLSVerify → Insecure (inverted semantics). - // Insecure is only meaningful for OCI repositories, but we set it - // consistently so the intent is clear. - if !src.Spec.TLSVerify { - dst.Spec.Insecure = true - } - - return dst -} - -// ApplyDesiredSpec updates an existing HelmRepository's spec fields to match -// the desired state. Returns true if any field was changed. -func ApplyDesiredSpec(existing *sourcev1.HelmRepository, desired *sourcev1.HelmRepository) bool { - changed := false - - if existing.Spec.URL != desired.Spec.URL { - existing.Spec.URL = desired.Spec.URL - changed = true - } - if existing.Spec.Type != desired.Spec.Type { - existing.Spec.Type = desired.Spec.Type - changed = true - } - if existing.Spec.Insecure != desired.Spec.Insecure { - existing.Spec.Insecure = desired.Spec.Insecure - changed = true - } - if existing.Spec.Interval != desired.Spec.Interval { - existing.Spec.Interval = desired.Spec.Interval - changed = true - } - - // Ensure labels are up to date. - if existing.Labels == nil { - existing.Labels = make(map[string]string) - } - for k, v := range desired.Labels { - if existing.Labels[k] != v { - existing.Labels[k] = v - changed = true - } - } - - return changed -} - // TODO: need to re-work these statuses according to adr -// MapInternalStatusToClusterConditions translates the internal HelmRepository -// status into conditions suitable for the HelmClusterRepository status. -func MapInternalStatusToClusterConditions(internal *sourcev1.HelmRepository) []metav1.Condition { +func MapInternalStatusToClusterConditions(internalConditions []metav1.Condition) []metav1.Condition { now := metav1.Now() - // Find the Ready condition on the internal resource. var readyCond *metav1.Condition - for i := range internal.Status.Conditions { - if internal.Status.Conditions[i].Type == ConditionTypeReady { - readyCond = &internal.Status.Conditions[i] + + for i := range internalConditions { + if internalConditions[i].Type == ConditionTypeReady { + readyCond = &internalConditions[i] + break } } @@ -124,7 +41,7 @@ func MapInternalStatusToClusterConditions(internal *sourcev1.HelmRepository) []m Type: ConditionTypeReady, Status: metav1.ConditionUnknown, Reason: ReasonInternalNotReady, - Message: "Initializing repository..", + Message: "Processing", LastTransitionTime: now, }, } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index 1cc1246..52d5260 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -18,8 +18,11 @@ package helmclusteraddonrepository import ( "context" + "errors" "fmt" + "net/url" + corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -29,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/werf/3p-fluxcd-pkg/apis/meta" sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) @@ -42,115 +46,338 @@ type Reconciler struct { func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger := log.FromContext(ctx).WithValues("helmclusterrepository", req.Name) - var clusterRepo helmv1alpha1.HelmClusterAddonRepository - if err := r.Client.Get(ctx, req.NamespacedName, &clusterRepo); err != nil { + var repo helmv1alpha1.HelmClusterAddonRepository + + if err := r.Client.Get(ctx, req.NamespacedName, &repo); err != nil { if apierrors.IsNotFound(err) { logger.Info("HelmClusterAddonRepository not found, skipping") + return reconcile.Result{}, nil } + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) } - if !clusterRepo.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, &clusterRepo) + parsedURL, err := url.Parse(repo.Spec.URL) + if err != nil { + return reconcile.Result{}, fmt.Errorf("cannot parse HelmClusterAddonRepository url: %w", err) + } + + repoType, err := GetRepositoryType(parsedURL.Scheme) + if err != nil { + return reconcile.Result{}, err } - if !controllerutil.ContainsFinalizer(&clusterRepo, FinalizerName) { - controllerutil.AddFinalizer(&clusterRepo, FinalizerName) - if err := r.Client.Update(ctx, &clusterRepo); err != nil { + if !repo.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &repo, repoType) + } + + if !controllerutil.ContainsFinalizer(&repo, FinalizerName) { + controllerutil.AddFinalizer(&repo, FinalizerName) + + if err := r.Client.Update(ctx, &repo); err != nil { return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) } + + return reconcile.Result{}, nil } - desired := BuildDesiredHelmRepository(&clusterRepo) + switch repoType { + case InternalHelmRepository: + return r.reconcileInternalHelmRepository(ctx, &repo) + case InternalOCIRepository: + return r.reconcileInternalOCIRepository(ctx, &repo) + default: + return reconcile.Result{}, nil + } +} - var existing sourcev1.HelmRepository - err := r.Client.Get(ctx, types.NamespacedName{ - Name: desired.Name, - Namespace: desired.Namespace, - }, &existing) +func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) - if apierrors.IsNotFound(err) { - logger.Info("Creating internal repository custom resource", "name", desired.Name, "namespace", desired.Namespace) - if err := r.Client.Create(ctx, desired); err != nil { - r.setCondition(&clusterRepo, metav1.ConditionFalse, ReasonMirrorFailed, - fmt.Sprintf("Failed to create internal custom resource: %v", err)) - _ = r.Client.Status().Update(ctx, &clusterRepo) - return reconcile.Result{}, fmt.Errorf("creating internal repository custom resource: %w", err) + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, InternalHelmRepository); err != nil { + return reconcile.Result{}, err + } + + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, InternalHelmRepository); err != nil { + return reconcile.Result{}, err + } + + existing := &sourcev1.HelmRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repo.Name, + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Interval = metav1.Duration{Duration: DefaultInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: GetRepositoryAuthSecretName(InternalHelmRepository, repo.Name), + } + existing.Spec.PassCredentials = true } - // After creation, the internal resource has no status yet. - r.setCondition(&clusterRepo, metav1.ConditionUnknown, ReasonMirrorSucceeded, - "Internal repository custom resource created, waiting for status") - if err := r.Client.Status().Update(ctx, &clusterRepo); err != nil { - return reconcile.Result{}, fmt.Errorf("updating status after create: %w", err) + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: GetRepositoryTLSSecretName(InternalHelmRepository, repo.Name), + } } - return reconcile.Result{}, nil + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + return nil + }) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling helm repository: %w", err), ReasonMirrorFailed) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled helm repository", "operation", op) + } + + return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) +} + +func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, InternalOCIRepository); err != nil { + return reconcile.Result{}, err } + + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, InternalOCIRepository); err != nil { + return reconcile.Result{}, err + } + + existing := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: repo.Name, + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.Spec.URL = repo.Spec.URL + existing.Spec.Interval = metav1.Duration{Duration: DefaultInterval} + existing.Spec.Insecure = !repo.Spec.TLSVerify + existing.Spec.CertSecretRef = nil + existing.Spec.SecretRef = nil + + if repo.Spec.Auth != nil { + existing.Spec.SecretRef = &meta.LocalObjectReference{ + Name: GetRepositoryAuthSecretName(InternalOCIRepository, repo.Name), + } + } + + if repo.Spec.CACertificate != "" { + existing.Spec.CertSecretRef = &meta.LocalObjectReference{ + Name: GetRepositoryTLSSecretName(InternalOCIRepository, repo.Name), + } + } + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + return nil + }) if err != nil { - return reconcile.Result{}, fmt.Errorf("getting internal repository custom resource: %w", err) + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling oci repository: %w", err), ReasonMirrorFailed) } - if ApplyDesiredSpec(&existing, desired) { - logger.Info("Updating internal repository custom resource spec", "name", existing.Name) - if err := r.Client.Update(ctx, &existing); err != nil { - r.setCondition(&clusterRepo, metav1.ConditionFalse, ReasonMirrorFailed, - fmt.Sprintf("Failed to update internal repository custom resource: %v", err)) - _ = r.Client.Status().Update(ctx, &clusterRepo) - return reconcile.Result{}, fmt.Errorf("updating internal custom resource: %w", err) + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled oci repository", "operation", op) + } + + return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) +} + +func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) error { + secretName := GetRepositoryAuthSecretName(repoType, repo.Name) + + if repo.Spec.Auth == nil { + if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { + return fmt.Errorf("cannot delete obsolete auth secret: %w", err) } + + return nil } - // 7. Propagate status from internal → cluster. - conditions := MapInternalStatusToClusterConditions(&existing) - clusterRepo.Status.Conditions = conditions - clusterRepo.Status.ObservedGeneration = clusterRepo.Generation - if err := r.Client.Status().Update(ctx, &clusterRepo); err != nil { - return reconcile.Result{}, fmt.Errorf("updating internal custom resource status: %w", err) + authSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: TargetNamespace, + }, } - return reconcile.Result{}, nil + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, authSecret, func() error { + authSecret.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + authSecret.StringData = map[string]string{ + "username": repo.Spec.Auth.Username, + "password": repo.Spec.Auth.Password, + } + + return nil + }); err != nil { + return fmt.Errorf("cannot reconcile auth secret: %w", err) + } + + return nil +} + +func (r *Reconciler) reconcileInternalRepositoryTLSSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) error { + secretName := GetRepositoryTLSSecretName(repoType, repo.Name) + + if repo.Spec.CACertificate == "" { + if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { + return fmt.Errorf("cannot delete obsolete tls secret: %w", err) + } + + return nil + } + + // TODO: consider adding CA certificate format validation + + tlsSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: TargetNamespace, + }, + } + + if _, err := controllerutil.CreateOrPatch(ctx, r.Client, tlsSecret, func() error { + tlsSecret.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + tlsSecret.StringData = map[string]string{ + "ca.crt": repo.Spec.CACertificate, + } + + return nil + }); err != nil { + return fmt.Errorf("cannot reconcile tls secret: %w", err) + } + + return nil +} + +// ensureResourceDeleted safely deletes an object if it exists. +func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + + return fmt.Errorf("checking existence of obsolete resource: %w", err) + } + + if err := r.Client.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("deleting obsolete resource: %w", err) + } + + return nil } // reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. -func (r *Reconciler) reconcileDelete(ctx context.Context, clusterRepo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { - logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", clusterRepo.Name) +func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) - if !controllerutil.ContainsFinalizer(clusterRepo, FinalizerName) { + if !controllerutil.ContainsFinalizer(repo, FinalizerName) { return reconcile.Result{}, nil } - // Delete the internal repository resource. - var internal sourcev1.HelmRepository - err := r.Client.Get(ctx, types.NamespacedName{ - Name: clusterRepo.Name, - Namespace: TargetNamespace, - }, &internal) + if err := r.ensureResourceDeleted( + ctx, + GetRepositoryAuthSecretName(repoType, repo.Name), + TargetNamespace, + &corev1.Secret{}, + ); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal auth secret: %w", err), ReasonCleanupFailed) + } - if err == nil { - logger.Info("Deleting internal repository resource", "name", internal.Name, "namespace", internal.Namespace) - if err := r.Client.Delete(ctx, &internal); err != nil && !apierrors.IsNotFound(err) { - r.setCondition(clusterRepo, metav1.ConditionFalse, ReasonCleanupFailed, - fmt.Sprintf("Failed to delete internal repository resource: %v", err)) - _ = r.Client.Status().Update(ctx, clusterRepo) - return reconcile.Result{}, fmt.Errorf("deleting internal repository resource: %w", err) - } - } else if !apierrors.IsNotFound(err) { - return reconcile.Result{}, fmt.Errorf("getting internal repository resource for deletion: %w", err) + if err := r.ensureResourceDeleted( + ctx, + GetRepositoryTLSSecretName(repoType, repo.Name), + TargetNamespace, + &corev1.Secret{}, + ); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal tls secret: %w", err), ReasonCleanupFailed) + } + + var internalRepository client.Object + + switch repoType { + case InternalHelmRepository: + internalRepository = &sourcev1.HelmRepository{} + case InternalOCIRepository: + internalRepository = &sourcev1.OCIRepository{} + default: + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("cannot remove unsupported repisotory type: %s", repoType), ReasonCleanupFailed) } - // Remove finalizer. - controllerutil.RemoveFinalizer(clusterRepo, FinalizerName) - if err := r.Client.Update(ctx, clusterRepo); err != nil { + if err := r.ensureResourceDeleted(ctx, repo.Name, TargetNamespace, internalRepository); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal repository: %w", err), ReasonCleanupFailed) + } + + controllerutil.RemoveFinalizer(repo, FinalizerName) + + if err := r.Client.Update(ctx, repo); err != nil { return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) } logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +// patchStatusError is a helper to safely patch a failure condition onto the cluster resource. +func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, reconcileErr error, reason string) error { + base := repo.DeepCopy() + + r.setCondition(repo, metav1.ConditionFalse, reason, reconcileErr.Error()) + + if patchErr := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); patchErr != nil { + return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) + } + + return reconcileErr +} + +// updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. +func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (reconcile.Result, error) { + base := repo.DeepCopy() + + repo.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) + repo.Status.ObservedGeneration = repo.Generation + + if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + } + return reconcile.Result{}, nil } // setCondition is a helper to set a single Ready condition on the cluster resource. func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, status metav1.ConditionStatus, reason, message string) { now := metav1.Now() + newCond := metav1.Condition{ Type: ConditionTypeReady, Status: status, @@ -160,16 +387,18 @@ func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, ObservedGeneration: repo.Generation, } - // Replace existing Ready condition or append. for i, c := range repo.Status.Conditions { if c.Type == ConditionTypeReady { // Only update LastTransitionTime if status actually changed. if c.Status == status { newCond.LastTransitionTime = c.LastTransitionTime } + repo.Status.Conditions[i] = newCond + return } } + repo.Status.Conditions = append(repo.Status.Conditions, newCond) } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go new file mode 100644 index 0000000..4abcb99 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go @@ -0,0 +1,57 @@ +/* +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 helmclusteraddonrepository + +import ( + "fmt" + "hash/fnv" + "strings" +) + +func GenerateSafeName(baseName string, maxLen int) string { + if len(baseName) <= maxLen { + return baseName + } + + h := fnv.New32a() + h.Write([]byte(baseName)) + hashStr := fmt.Sprintf("%x", h.Sum32())[:5] + + truncated := baseName[:maxLen-6] + truncated = strings.TrimRight(truncated, "-") + + return fmt.Sprintf("%s-%s", truncated, hashStr) +} + +func GetRepositoryAuthSecretName(repoType InternalRepositoryType, internalRepoName string) string { + return GenerateSafeName("auth-"+string(repoType)+"-"+internalRepoName, 63) +} + +func GetRepositoryTLSSecretName(repoType InternalRepositoryType, internalRepoName string) string { + return GenerateSafeName("tls-"+string(repoType)+"-"+internalRepoName, 63) +} + +func GetRepositoryType(scheme string) (InternalRepositoryType, error) { + switch scheme { + case "http", "https": + return InternalHelmRepository, nil + case "oci": + return InternalOCIRepository, nil + default: + return "", fmt.Errorf("unsuppored repository schema in use: %s", scheme) + } +} From a9567eb7166550af931b9b16b573dfbc62286c33 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Mon, 2 Mar 2026 13:03:47 +0300 Subject: [PATCH 11/26] feat: add HelmClusterAddonChart reconcilation Signed-off-by: Ilya Drey --- api/v1alpha1/helm_cluster_addon_chart.go | 2 + crds/helmclusteraddoncharts.yaml | 4 + images/operator-helm-artifact/go.mod | 55 +++- images/operator-helm-artifact/go.sum | 290 +++++++++++++++++- .../pkg/api/openapi/zz_generated.openapi.go | 10 +- .../helmclusteraddonrepository/client.go | 111 +++++++ .../helmclusteraddonrepository/constants.go | 4 +- .../helmclusteraddonrepository/reconciler.go | 65 ++++ .../helmclusteraddonrepository/utils.go | 6 +- 9 files changed, 531 insertions(+), 16 deletions(-) create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go diff --git a/api/v1alpha1/helm_cluster_addon_chart.go b/api/v1alpha1/helm_cluster_addon_chart.go index 5ed8b0b..a2c4e9a 100644 --- a/api/v1alpha1/helm_cluster_addon_chart.go +++ b/api/v1alpha1/helm_cluster_addon_chart.go @@ -67,6 +67,8 @@ type HelmClusterAddonChartVersion struct { // Helm chart digest // +kubebuilder:validation:MinLength=1 Digest string `json:"digest"` + // Chart pulled from repository + Pulled bool `json:"pulled"` } // HelmClusterAddonChartList contains a list of HelmClusterAddonCharts. diff --git a/crds/helmclusteraddoncharts.yaml b/crds/helmclusteraddoncharts.yaml index ba6d15c..75bd871 100644 --- a/crds/helmclusteraddoncharts.yaml +++ b/crds/helmclusteraddoncharts.yaml @@ -112,12 +112,16 @@ spec: description: Helm chart digest minLength: 1 type: string + pulled: + description: Chart pulled from repository + type: boolean version: description: Helm chart version minLength: 1 type: string required: - digest + - pulled - version type: object type: array diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod index 4fefdc4..d93fe05 100644 --- a/images/operator-helm-artifact/go.mod +++ b/images/operator-helm-artifact/go.mod @@ -6,21 +6,38 @@ replace github.com/deckhouse/operator-helm/api => ../../api require ( github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 + github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 github.com/werf/nelm-source-controller/api v0.1.4 + go.yaml.in/yaml/v3 v3.0.4 + helm.sh/helm/v3 v3.20.0 + k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/controller-runtime v0.23.1 ) require ( + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/containerd/containerd v1.7.30 // indirect + github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -30,42 +47,64 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/inconshreveable/mousetrap v1.1.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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/term v0.5.2 // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // 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/peterbourgon/diskv v2.0.1+incompatible // 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.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect - github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 // indirect github.com/x448/float16 v0.8.4 // indirect + github.com/xlab/treeprint v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.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/time v0.9.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.1 // indirect k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/cli-runtime v0.35.0 // indirect + k8s.io/component-base v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + k8s.io/kubectl v0.35.0 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/images/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum index 3fa068b..407ad93 100644 --- a/images/operator-helm-artifact/go.sum +++ b/images/operator-helm-artifact/go.sum @@ -1,79 +1,359 @@ +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= +github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= +github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= +github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= +github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+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/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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/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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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/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-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= +github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= +github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/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.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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 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.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= +github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= 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.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= 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.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= +github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= +github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +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.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/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/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= +github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1/go.mod h1:00dBUg4SN+4Xu4LWrbQm5LdmRKVP9Fjbvb+rvqjHrVI= github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= +github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1/go.mod h1:14co1+Ub5rW0Bp3Qo4IzCHwEcaw06StyMu7Rv5pMVCY= github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= +github.com/werf/nelm-source-controller/api v0.1.4/go.mod h1:++j7xw4YVDE8gR9x1HWhIagpo68jE1oEd4+6tMAgXgs= 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/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= +go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 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= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +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/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= +golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +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.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= +golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= +google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/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.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +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= +helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= +helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= +k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= +k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= +k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= 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-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= +k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index 8ea159b..fd385b3 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -366,8 +366,16 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(re Format: "", }, }, + "pulled": { + SchemaProps: spec.SchemaProps{ + Description: "Chart pulled from repository", + Default: false, + Type: []string{"boolean"}, + Format: "", + }, + }, }, - Required: []string{"version", "digest"}, + Required: []string{"version", "digest", "pulled"}, }, }, } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go new file mode 100644 index 0000000..37b1651 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/client.go @@ -0,0 +1,111 @@ +/* +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 helmclusteraddonrepository + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "go.yaml.in/yaml/v3" + "k8s.io/apimachinery/pkg/util/wait" +) + +var HelmRepositoryDefaultClient HelmRepositoryClient + +type HelmRepositoryClient struct{} + +func (c *HelmRepositoryClient) FetchCharts(ctx context.Context, url string) (map[string][]helmv1alpha1.HelmClusterAddonChartVersion, error) { + if !strings.HasSuffix(url, "/index.yaml") { + url += "/index.yaml" + } + + var indexFile HelmRepositoryIndex + + backoff := wait.Backoff{ + Duration: 1 * time.Second, // Initial delay + Factor: 2.0, // Double the delay each time + Jitter: 0.1, // Add 10% randomness to prevent the thundering herd problem + Steps: 3, // Maximum number of retries (1s, 2s, 4s, 8s, 16s) + } + + ctx, cancel := context.WithTimeout(ctx, 8*time.Second) + defer cancel() + + err := wait.ExponentialBackoffWithContext(ctx, backoff, func(ctx context.Context) (done bool, err error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return true, fmt.Errorf("creating request: %w", err) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, nil + } + defer resp.Body.Close() + + if resp.StatusCode >= 500 { + return false, nil + } + + if resp.StatusCode >= 400 { + return true, fmt.Errorf("fatal client error: received status %d", resp.StatusCode) + } + + if err := yaml.NewDecoder(resp.Body).Decode(&indexFile); err != nil { + return true, fmt.Errorf("cannot decode response: %w", err) + } + + return true, nil + }) + if err != nil { + return nil, fmt.Errorf("helm repository index.yaml request failed: %w", err) + } + + charts := make(map[string][]helmv1alpha1.HelmClusterAddonChartVersion) + + for chartName, chartInfo := range indexFile.Entries { + charts[chartName] = make([]helmv1alpha1.HelmClusterAddonChartVersion, 0) + + for _, chartVersion := range chartInfo { + if chartVersion.Removed { + continue + } + + charts[chartName] = append(charts[chartName], helmv1alpha1.HelmClusterAddonChartVersion{ + Version: chartVersion.Version, + Digest: chartVersion.Digest, + }) + } + } + + return charts, nil +} + +type HelmRepositoryIndex struct { + APIVersion string `json:"apiVersion"` + Entries map[string][]HelmRepositoryChartVersion `json:"entries"` +} + +type HelmRepositoryChartVersion struct { + Version string `json:"version"` + Digest string `json:"digest"` + Removed bool `json:"removed,omitempty"` +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index 2129c74..7e7f745 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -44,6 +44,8 @@ const ( // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. ReasonMirrorFailed = "MirrorFailed" + ReasonChartsSyncFailed = "ChartsSyncFailed" + // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. ReasonInternalNotReady = "InternalNotReady" @@ -60,7 +62,7 @@ const ( LabelManagedByValue = "operator-helm" // LabelSourceName stores the name of the source HelmClusterAddonRepository. - LabelSourceName = "helm.deckhouse.io/source-name" + LabelSourceName = "helm.deckhouse.io/cluster-addon-repository-name" // DefaultInterval is the default reconciliation interval for the internal repository. DefaultInterval = 5 * time.Second diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index 52d5260..f38d5b0 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -21,11 +21,13 @@ import ( "errors" "fmt" "net/url" + "time" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" @@ -141,6 +143,10 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling helm repository: %w", err), ReasonMirrorFailed) } + if err := r.reconcileHelmRepositoryCharts(ctx, repo); err != nil { + return reconcile.Result{RequeueAfter: 30 * time.Second}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling helm repository charts: %w", err), ReasonChartsSyncFailed) + } + if op != controllerutil.OperationResultNone { logger.Info("Successfully reconciled helm repository", "operation", op) } @@ -148,6 +154,65 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) } +func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) error { + logger := log.FromContext(ctx) + + charts, err := HelmRepositoryDefaultClient.FetchCharts(ctx, repo.Spec.URL) + if err != nil { + return fmt.Errorf("cannot fetch chart info from repository: %w", err) + } + + for chart, versions := range charts { + existing := &helmv1alpha1.HelmClusterAddonChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: GetHelmClusterAddonChartName(repo.Name, chart), + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + existing.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: repo.APIVersion, + Kind: repo.Kind, + Name: repo.Name, + UID: repo.UID, + Controller: ptr.To(true), + BlockOwnerDeletion: ptr.To(true), + }, + } + + existing.Labels = map[string]string{ + LabelManagedBy: LabelManagedByValue, + LabelSourceName: repo.Name, + } + + existingVersionsMap := make(map[string]helmv1alpha1.HelmClusterAddonChartVersion) + for _, version := range existing.Status.Versions { + existingVersionsMap[version.Version] = version + } + + for i, version := range versions { + if existingVersion, found := existingVersionsMap[version.Version]; found && version.Digest == existingVersion.Digest { + versions[i].Pulled = existingVersion.Pulled + } + } + + existing.Status.Versions = versions + + return nil + }) + if err != nil { + return fmt.Errorf("cannot create or update helm chart info: %w", err) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled helm repository chart", "operation", op, "repository", repo.Name, "chart", chart) + } + } + + return nil +} + func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go index 4abcb99..ac1d58d 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go @@ -52,6 +52,10 @@ func GetRepositoryType(scheme string) (InternalRepositoryType, error) { case "oci": return InternalOCIRepository, nil default: - return "", fmt.Errorf("unsuppored repository schema in use: %s", scheme) + return "", fmt.Errorf("unsupported repository schema in use: %s", scheme) } } + +func GetHelmClusterAddonChartName(repoName, chartName string) string { + return GenerateSafeName(repoName+"-"+chartName, 63) +} From 3d0aa6c7ea081d519a78808bd62d6a94094f1273 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Mon, 2 Mar 2026 18:32:57 +0300 Subject: [PATCH 12/26] feat: add HelmClusterAddon reconciliation Signed-off-by: Ilya Drey --- .../cmd/operator-helm-controller/main.go | 8 + images/operator-helm-artifact/go.mod | 40 +-- images/operator-helm-artifact/go.sum | 185 +--------- .../controller/helmclusteraddon/constants.go | 52 +++ .../controller/helmclusteraddon/controller.go | 85 +++++ .../pkg/controller/helmclusteraddon/mapper.go | 64 ++++ .../controller/helmclusteraddon/reconciler.go | 317 ++++++++++++++++++ .../helmclusteraddonrepository/constants.go | 7 - .../helmclusteraddonrepository/reconciler.go | 49 ++- .../helmclusteraddonrepository/utils.go | 61 ---- .../operator-helm-artifact/pkg/utils/name.go | 112 +++++++ .../pkg/utils/repository.go | 45 +++ 12 files changed, 710 insertions(+), 315 deletions(-) create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go delete mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go create mode 100644 images/operator-helm-artifact/pkg/utils/name.go create mode 100644 images/operator-helm-artifact/pkg/utils/repository.go diff --git a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go index 43800fc..c7babc3 100644 --- a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -20,6 +20,7 @@ import ( "flag" "os" + helmv2 "github.com/werf/3p-helm-controller/api/v2" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth" @@ -29,6 +30,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddon" "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonrepository" sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) @@ -39,6 +41,7 @@ func init() { _ = clientgoscheme.AddToScheme(scheme) _ = helmv1alpha1.AddToScheme(scheme) _ = sourcev1.AddToScheme(scheme) + _ = helmv2.AddToScheme(scheme) } func main() { @@ -80,6 +83,11 @@ func main() { os.Exit(1) } + if err := helmclusteraddon.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddon controller") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { logger.Error(err, "unable to set up health check") os.Exit(1) diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod index d93fe05..2eea7c8 100644 --- a/images/operator-helm-artifact/go.mod +++ b/images/operator-helm-artifact/go.mod @@ -7,9 +7,9 @@ replace github.com/deckhouse/operator-helm/api => ../../api require ( github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 + github.com/werf/3p-helm-controller/api v0.1.4 github.com/werf/nelm-source-controller/api v0.1.4 go.yaml.in/yaml/v3 v3.0.4 - helm.sh/helm/v3 v3.20.0 k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 @@ -19,25 +19,14 @@ require ( ) require ( - github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/MakeNowJust/heredoc v1.0.0 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.30 // indirect - github.com/containerd/errdefs v0.3.0 // indirect - github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect - github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect - github.com/go-errors/errors v1.4.2 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect @@ -47,41 +36,24 @@ require ( github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/inconshreveable/mousetrap v1.1.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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/moby/term v0.5.2 // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // 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/peterbourgon/diskv v2.0.1+incompatible // 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.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect + github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 // indirect github.com/x448/float16 v0.8.4 // indirect - github.com/xlab/treeprint v1.2.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.0 // indirect @@ -90,21 +62,13 @@ require ( golang.org/x/text v0.33.0 // indirect golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect - google.golang.org/grpc v1.72.2 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.1 // indirect - k8s.io/cli-runtime v0.35.0 // indirect - k8s.io/component-base v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kubectl v0.35.0 // indirect - oras.land/oras-go/v2 v2.6.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect - sigs.k8s.io/kustomize/api v0.20.1 // indirect - sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect sigs.k8s.io/yaml v1.6.0 // indirect diff --git a/images/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum index 407ad93..e631ff7 100644 --- a/images/operator-helm-artifact/go.sum +++ b/images/operator-helm-artifact/go.sum @@ -1,75 +1,26 @@ -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= -github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= -github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= -github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= -github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= -github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= -github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= -github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= -github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= -github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= -github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -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/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= -github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= -github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= -github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker-credential-helpers v0.8.2 h1:bX3YxiGzFP5sOXWc3bTPEXdEaZSeVMrFgOr3T+zrFAo= -github.com/docker/docker-credential-helpers v0.8.2/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= -github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= -github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= github.com/evanphx/json-patch v5.9.11+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/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= -github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= -github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= 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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 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= @@ -82,8 +33,6 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr 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/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.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= @@ -97,25 +46,6 @@ github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4en github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= 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/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= -github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= -github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= -github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= -github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= -github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= -github.com/hashicorp/golang-lru/v2 v2.0.5 h1:wW7h1TG88eUIJ2i69gaE3uNVtEPIagzhGvHgwfx2Vm4= -github.com/hashicorp/golang-lru/v2 v2.0.5/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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= @@ -131,40 +61,20 @@ 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/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= -github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= -github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= -github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= -github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= 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.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= -github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= -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= @@ -176,23 +86,8 @@ github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9Z github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho= -github.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc= -github.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ= -github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= -github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -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.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -201,8 +96,6 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -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= @@ -210,60 +103,15 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1/go.mod h1:00dBUg4SN+4Xu4LWrbQm5LdmRKVP9Fjbvb+rvqjHrVI= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 h1:edZ5ugpeUvmjG+g9laet8qTBqDdQPl18aNr6k0xqdYY= github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1/go.mod h1:14co1+Ub5rW0Bp3Qo4IzCHwEcaw06StyMu7Rv5pMVCY= +github.com/werf/3p-helm-controller/api v0.1.4 h1:s7g9UQOrDMUzVE+JtWOP2xApnPOKYlNe1tXkkWCisAw= +github.com/werf/3p-helm-controller/api v0.1.4/go.mod h1:tiPvDerlc5SwKIDmXB8L3kIMJHse+wigueoEGQq+588= github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= github.com/werf/nelm-source-controller/api v0.1.4/go.mod h1:++j7xw4YVDE8gR9x1HWhIagpo68jE1oEd4+6tMAgXgs= 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/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= -github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= -go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= -go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= -go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU= -go.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= -go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0 h1:SZmDnHcgp3zwlPBS2JX2urGYe/jBKEIT6ZedHRUyCz8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.32.0/go.mod h1:fdWW0HtZJ7+jNpTKUR0GpMEDP69nR8YBJQxNiVCE3jk= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= -go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= -go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= -go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= -go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= -go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= 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= @@ -274,8 +122,6 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -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/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= @@ -284,8 +130,6 @@ 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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= @@ -298,13 +142,6 @@ golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= 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/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb h1:p31xT4yrYrSM/G4Sn2+TNUkVhFCbG9y8itM2S6Th950= -google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= -google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -314,43 +151,27 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.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.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -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= -helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= -helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= k8s.io/apiextensions-apiserver v0.35.1/go.mod h1:2CN4fe1GZ3HMe4wBr25qXyJnJyZaquy4nNlNmb3R7AQ= k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= -k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= -k8s.io/component-base v0.35.1 h1:XgvpRf4srp037QWfGBLFsYMUQJkE5yMa94UsJU7pmcE= -k8s.io/component-base v0.35.1/go.mod h1:HI/6jXlwkiOL5zL9bqA3en1Ygv60F03oEpnuU1G56Bs= 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-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= -k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= -oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= -sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= -sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= -sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= -sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go new file mode 100644 index 0000000..ed9b07d --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -0,0 +1,52 @@ +/* +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 helmclusteraddon + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddon-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + // FinalizerName is the finalizer added to HelmClusterRepository to ensure cleanup. + FinalizerName = "helm.deckhouse.io/cleanup" + + // ConditionTypeReady is the condition type for readiness. + ConditionTypeReady = "Ready" + + // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. + ReasonMirrorSucceeded = "MirrorSucceeded" + + // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. + ReasonMirrorFailed = "MirrorFailed" + + // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. + ReasonInternalNotReady = "InternalNotReady" + + // ReasonInternalReady indicates the internal HelmRepository has reported Ready. + ReasonInternalReady = "InternalReady" + + // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. + ReasonCleanupFailed = "CleanupFailed" + + ReasonProcessing = "Processing" + + LabelManagedBy = "helm.deckhouse.io/managed-by" + LabelManagedByValue = "operator-helm" + LabelSourceName = "helm.deckhouse.io/cluster-addon-name" +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go new file mode 100644 index 0000000..32720fd --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go @@ -0,0 +1,85 @@ +/* +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 helmclusteraddon + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + For(&helmv1alpha1.HelmClusterAddon{}). + Watches( + &sourcev1.HelmChart{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Watches( + &helmv2.HelmRelease{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Complete(r) +} + +func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + if obj.GetNamespace() != TargetNamespace { + return nil + } + + labels := obj.GetLabels() + if labels[LabelManagedBy] != LabelManagedByValue { + return nil + } + + sourceName := labels[LabelSourceName] + if sourceName == "" { + logger.Info("Internal repository resource missing source-name label, skipping", + "name", obj.GetName(), "namespace", obj.GetNamespace()) + + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: sourceName, + Namespace: "", + }, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go new file mode 100644 index 0000000..1095617 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go @@ -0,0 +1,64 @@ +/* +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 helmclusteraddon + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TODO: need to re-work these statuses according to adr + +func MapInternalStatusToClusterConditions(internalConditions []metav1.Condition) []metav1.Condition { + now := metav1.Now() + + var readyCond *metav1.Condition + + for i := range internalConditions { + if internalConditions[i].Type == ConditionTypeReady { + readyCond = &internalConditions[i] + + break + } + } + + if readyCond == nil { + return []metav1.Condition{ + { + Type: ConditionTypeReady, + Status: metav1.ConditionUnknown, + Reason: ReasonInternalNotReady, + Message: "Processing", + LastTransitionTime: now, + }, + } + } + + reason := ReasonInternalNotReady + if readyCond.Status == metav1.ConditionTrue { + reason = ReasonInternalReady + } + + return []metav1.Condition{ + { + Type: ConditionTypeReady, + Status: readyCond.Status, + Reason: reason, + Message: readyCond.Message, + LastTransitionTime: now, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go new file mode 100644 index 0000000..b7f6aa5 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -0,0 +1,317 @@ +/* +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 helmclusteraddon + +import ( + "context" + "errors" + "fmt" + + "github.com/deckhouse/operator-helm/pkg/utils" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +type Reconciler struct { + Client client.Client +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddon", req.Name) + + var release helmv1alpha1.HelmClusterAddon + + if err := r.Client.Get(ctx, req.NamespacedName, &release); err != nil { + if apierrors.IsNotFound(err) { + logger.Info("HelmClusterAddon not found, skipping") + + return reconcile.Result{}, nil + } + + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddon: %w", err) + } + + if !release.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &release) + } + + if !controllerutil.ContainsFinalizer(&release, FinalizerName) { + controllerutil.AddFinalizer(&release, FinalizerName) + + if err := r.Client.Update(ctx, &release); err != nil { + return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) + } + + return reconcile.Result{}, nil + } + + var repo helmv1alpha1.HelmClusterAddonRepository + + if err := r.Client.Get(ctx, types.NamespacedName{Name: release.Spec.Chart.HelmClusterAddonRepository}, &repo); err != nil { + // TODO: rework this condition + if apierrors.IsNotFound(err) { + return reconcile.Result{RequeueAfter: 0}, r.patchStatusError(ctx, &release, fmt.Errorf("repository not found: %w", err), ReasonMirrorFailed) + } + + return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) + } + + if err := r.reconcileInternalHelmChart(ctx, &release, &repo); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, &release, fmt.Errorf("internal helm chart reconcile failed: %w", err), ReasonMirrorFailed) + } + + return r.reconcileInternalRelease(ctx, &release) +} + +func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { + logger := log.FromContext(ctx) + + var addonChart helmv1alpha1.HelmClusterAddonChart + + if err := r.Client.Get( + ctx, + types.NamespacedName{ + Name: utils.GetHelmClusterAddonChartName(release.Spec.Chart.HelmClusterAddonRepository, + release.Spec.Chart.HelmClusterAddonRepository), + }, + &addonChart, + ); err != nil { + if apierrors.IsNotFound(err) { + return fmt.Errorf("addon chart not found: %w", err) + } + + return fmt.Errorf("getting HelmClusterAddonChart: %w", err) + } + + // TODO: implement logic depending on pulled flag in the HelmClusterAddonChart + //for _, chartInfo := range addonChart.Status.Versions { + // if chartInfo.Pulled && chartInfo.Version == release.Spec.Chart.Version { + // + // } + //} + + repoType, _ := utils.GetRepositoryType(repo.Spec.URL) + + existing := &sourcev1.HelmChart{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.GetInternalHelmChartName( + release.Name, + release.Spec.Chart.HelmClusterAddonChartName, + release.Spec.Chart.Version), + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[LabelManagedBy] = LabelManagedByValue + existing.Labels[LabelSourceName] = release.Name + + existing.Spec.Chart = release.Spec.Chart.HelmClusterAddonChartName + existing.Spec.Version = release.Spec.Chart.Version + + switch repoType { + case utils.InternalHelmRepository: + existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.HelmRepositoryKind, + Name: release.Spec.Chart.HelmClusterAddonRepository, + } + case utils.InternalOCIRepository: + existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ + Kind: sourcev1.OCIRepositoryKind, + Name: release.Spec.Chart.HelmClusterAddonRepository, + } + default: + return fmt.Errorf("invalid repository type: %s", repoType) + } + + return nil + }) + if err != nil { + return fmt.Errorf("cannot create or update internal release: %w", err) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled internal helm chart", "operation", op, "repository", repo.Name, "chart", release.Spec.Chart.HelmClusterAddonChartName) + } + + return nil +} + +func (r *Reconciler) reconcileInternalRelease(ctx context.Context, release *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx) + + existing := &helmv2.HelmRelease{ + ObjectMeta: metav1.ObjectMeta{ + Name: release.Name, + Namespace: TargetNamespace, + }, + } + + op, err := controllerutil.CreateOrPatch(ctx, r.Client, existing, func() error { + if existing.Labels == nil { + existing.Labels = map[string]string{} + } + + existing.Labels[LabelManagedBy] = LabelManagedByValue + existing.Labels[LabelSourceName] = release.Name + + existing.Spec.ReleaseName = release.Name + existing.Spec.TargetNamespace = release.Spec.Namespace + existing.Spec.Values = release.Spec.Values + + existing.Spec.DriftDetection = &helmv2.DriftDetection{ + Mode: helmv2.DriftDetectionWarn, + } + + if release.Spec.Maintanace != "" { + existing.Spec.DriftDetection.Mode = helmv2.DriftDetectionEnabled + } + + existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ + Kind: sourcev1.HelmChartKind, + Name: utils.GetInternalHelmChartName( + release.Name, + release.Spec.Chart.HelmClusterAddonChartName, + release.Spec.Chart.Version), + Namespace: TargetNamespace, + } + + return nil + }) + if err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("reconcile internal helm release: %w", err), ReasonMirrorFailed) + } + + if op != controllerutil.OperationResultNone { + logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", release.Spec.Chart.HelmClusterAddonChartName) + } + + return r.updateSuccessStatus(ctx, release, existing.Status.Conditions) +} + +// ensureResourceDeleted safely deletes an object if it exists. +func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { + if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + + return fmt.Errorf("checking existence of obsolete resource: %w", err) + } + + if err := r.Client.Delete(ctx, obj); err != nil && !apierrors.IsNotFound(err) { + return fmt.Errorf("deleting obsolete resource: %w", err) + } + + return nil +} + +// reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. +func (r *Reconciler) reconcileDelete(ctx context.Context, release *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddon", release.Name) + + if !controllerutil.ContainsFinalizer(release, FinalizerName) { + return reconcile.Result{}, nil + } + + if err := r.ensureResourceDeleted(ctx, release.Name, TargetNamespace, &helmv2.HelmRelease{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("deleting internal helm release: %w", err), ReasonCleanupFailed) + } + + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(release.Spec.Chart.HelmClusterAddonRepository, release.Spec.Chart.HelmClusterAddonChartName, release.Spec.Chart.Version), TargetNamespace, &sourcev1.HelmChart{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("deleting internal helm chart: %w", err), ReasonCleanupFailed) + } + + controllerutil.RemoveFinalizer(release, FinalizerName) + + if err := r.Client.Update(ctx, release); err != nil { + return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) + } + + logger.Info("Cleanup complete") + + return reconcile.Result{}, nil +} + +// patchStatusError is a helper to safely patch a failure condition onto the cluster resource. +func (r *Reconciler) patchStatusError(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { + base := release.DeepCopy() + + r.setCondition(release, metav1.ConditionFalse, reason, reconcileErr.Error()) + + if patchErr := r.Client.Status().Patch(ctx, release, client.MergeFrom(base)); patchErr != nil { + return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) + } + + return reconcileErr +} + +// updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. +func (r *Reconciler) updateSuccessStatus(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, internalConditions []metav1.Condition) (reconcile.Result, error) { + base := release.DeepCopy() + + release.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) + release.Status.ObservedGeneration = release.Generation + + if err := r.Client.Status().Patch(ctx, release, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + } + + return reconcile.Result{}, nil +} + +// setCondition is a helper to set a single Ready condition on the cluster resource. +func (r *Reconciler) setCondition(release *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string) { + now := metav1.Now() + + newCond := metav1.Condition{ + Type: ConditionTypeReady, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: now, + ObservedGeneration: release.Generation, + } + + for i, c := range release.Status.Conditions { + if c.Type == ConditionTypeReady { + // Only update LastTransitionTime if status actually changed. + if c.Status == status { + newCond.LastTransitionTime = c.LastTransitionTime + } + + release.Status.Conditions[i] = newCond + + return + } + } + + release.Status.Conditions = append(release.Status.Conditions, newCond) +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index 7e7f745..4a99aed 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -18,13 +18,6 @@ package helmclusteraddonrepository import "time" -type InternalRepositoryType string - -const ( - InternalHelmRepository InternalRepositoryType = "helm" - InternalOCIRepository InternalRepositoryType = "oci" -) - const ( // ControllerName is the name of this controller, used for leader election and logging. ControllerName = "helmclusteraddonrepository-controller" diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index f38d5b0..01a65f2 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -20,9 +20,9 @@ import ( "context" "errors" "fmt" - "net/url" "time" + "github.com/deckhouse/operator-helm/pkg/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -60,12 +60,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) } - parsedURL, err := url.Parse(repo.Spec.URL) - if err != nil { - return reconcile.Result{}, fmt.Errorf("cannot parse HelmClusterAddonRepository url: %w", err) - } - - repoType, err := GetRepositoryType(parsedURL.Scheme) + repoType, err := utils.GetRepositoryType(repo.Spec.URL) if err != nil { return reconcile.Result{}, err } @@ -85,9 +80,9 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } switch repoType { - case InternalHelmRepository: + case utils.InternalHelmRepository: return r.reconcileInternalHelmRepository(ctx, &repo) - case InternalOCIRepository: + case utils.InternalOCIRepository: return r.reconcileInternalOCIRepository(ctx, &repo) default: return reconcile.Result{}, nil @@ -97,11 +92,11 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) - if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, InternalHelmRepository); err != nil { + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, utils.InternalHelmRepository); err != nil { return reconcile.Result{}, err } - if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, InternalHelmRepository); err != nil { + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, utils.InternalHelmRepository); err != nil { return reconcile.Result{}, err } @@ -121,14 +116,14 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * if repo.Spec.Auth != nil { existing.Spec.SecretRef = &meta.LocalObjectReference{ - Name: GetRepositoryAuthSecretName(InternalHelmRepository, repo.Name), + Name: utils.GetInternalRepositoryAuthSecretName(utils.InternalHelmRepository, repo.Name), } existing.Spec.PassCredentials = true } if repo.Spec.CACertificate != "" { existing.Spec.CertSecretRef = &meta.LocalObjectReference{ - Name: GetRepositoryTLSSecretName(InternalHelmRepository, repo.Name), + Name: utils.GetInternalRepositoryTLSSecretName(utils.InternalHelmRepository, repo.Name), } } @@ -165,7 +160,7 @@ func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *he for chart, versions := range charts { existing := &helmv1alpha1.HelmClusterAddonChart{ ObjectMeta: metav1.ObjectMeta{ - Name: GetHelmClusterAddonChartName(repo.Name, chart), + Name: utils.GetHelmClusterAddonChartName(repo.Name, chart), }, } @@ -216,11 +211,11 @@ func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *he func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) - if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, InternalOCIRepository); err != nil { + if err := r.reconcileInternalRepositoryAuthSecret(ctx, repo, utils.InternalOCIRepository); err != nil { return reconcile.Result{}, err } - if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, InternalOCIRepository); err != nil { + if err := r.reconcileInternalRepositoryTLSSecret(ctx, repo, utils.InternalOCIRepository); err != nil { return reconcile.Result{}, err } @@ -240,13 +235,13 @@ func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *h if repo.Spec.Auth != nil { existing.Spec.SecretRef = &meta.LocalObjectReference{ - Name: GetRepositoryAuthSecretName(InternalOCIRepository, repo.Name), + Name: utils.GetInternalRepositoryAuthSecretName(utils.InternalOCIRepository, repo.Name), } } if repo.Spec.CACertificate != "" { existing.Spec.CertSecretRef = &meta.LocalObjectReference{ - Name: GetRepositoryTLSSecretName(InternalOCIRepository, repo.Name), + Name: utils.GetInternalRepositoryTLSSecretName(utils.InternalOCIRepository, repo.Name), } } @@ -268,8 +263,8 @@ func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *h return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) } -func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) error { - secretName := GetRepositoryAuthSecretName(repoType, repo.Name) +func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) error { + secretName := utils.GetInternalRepositoryAuthSecretName(repoType, repo.Name) if repo.Spec.Auth == nil { if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { @@ -305,8 +300,8 @@ func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, return nil } -func (r *Reconciler) reconcileInternalRepositoryTLSSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) error { - secretName := GetRepositoryTLSSecretName(repoType, repo.Name) +func (r *Reconciler) reconcileInternalRepositoryTLSSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) error { + secretName := utils.GetInternalRepositoryTLSSecretName(repoType, repo.Name) if repo.Spec.CACertificate == "" { if err := r.ensureResourceDeleted(ctx, secretName, TargetNamespace, &corev1.Secret{}); err != nil { @@ -361,7 +356,7 @@ func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace } // reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. -func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType InternalRepositoryType) (reconcile.Result, error) { +func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) (reconcile.Result, error) { logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) if !controllerutil.ContainsFinalizer(repo, FinalizerName) { @@ -370,7 +365,7 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel if err := r.ensureResourceDeleted( ctx, - GetRepositoryAuthSecretName(repoType, repo.Name), + utils.GetInternalRepositoryAuthSecretName(repoType, repo.Name), TargetNamespace, &corev1.Secret{}, ); err != nil { @@ -379,7 +374,7 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel if err := r.ensureResourceDeleted( ctx, - GetRepositoryTLSSecretName(repoType, repo.Name), + utils.GetInternalRepositoryTLSSecretName(repoType, repo.Name), TargetNamespace, &corev1.Secret{}, ); err != nil { @@ -389,9 +384,9 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel var internalRepository client.Object switch repoType { - case InternalHelmRepository: + case utils.InternalHelmRepository: internalRepository = &sourcev1.HelmRepository{} - case InternalOCIRepository: + case utils.InternalOCIRepository: internalRepository = &sourcev1.OCIRepository{} default: return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("cannot remove unsupported repisotory type: %s", repoType), ReasonCleanupFailed) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go deleted file mode 100644 index ac1d58d..0000000 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/utils.go +++ /dev/null @@ -1,61 +0,0 @@ -/* -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 helmclusteraddonrepository - -import ( - "fmt" - "hash/fnv" - "strings" -) - -func GenerateSafeName(baseName string, maxLen int) string { - if len(baseName) <= maxLen { - return baseName - } - - h := fnv.New32a() - h.Write([]byte(baseName)) - hashStr := fmt.Sprintf("%x", h.Sum32())[:5] - - truncated := baseName[:maxLen-6] - truncated = strings.TrimRight(truncated, "-") - - return fmt.Sprintf("%s-%s", truncated, hashStr) -} - -func GetRepositoryAuthSecretName(repoType InternalRepositoryType, internalRepoName string) string { - return GenerateSafeName("auth-"+string(repoType)+"-"+internalRepoName, 63) -} - -func GetRepositoryTLSSecretName(repoType InternalRepositoryType, internalRepoName string) string { - return GenerateSafeName("tls-"+string(repoType)+"-"+internalRepoName, 63) -} - -func GetRepositoryType(scheme string) (InternalRepositoryType, error) { - switch scheme { - case "http", "https": - return InternalHelmRepository, nil - case "oci": - return InternalOCIRepository, nil - default: - return "", fmt.Errorf("unsupported repository schema in use: %s", scheme) - } -} - -func GetHelmClusterAddonChartName(repoName, chartName string) string { - return GenerateSafeName(repoName+"-"+chartName, 63) -} diff --git a/images/operator-helm-artifact/pkg/utils/name.go b/images/operator-helm-artifact/pkg/utils/name.go new file mode 100644 index 0000000..a143709 --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/name.go @@ -0,0 +1,112 @@ +/* +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 utils + +import ( + "fmt" + "hash/fnv" + "strings" +) + +func GetHash(s string) string { + h := fnv.New32a() + + _, _ = h.Write([]byte(s)) + + return fmt.Sprintf("%x", h.Sum32())[:5] +} + +func GetInternalRepositoryAuthSecretName(repoType InternalRepositoryType, internalRepoName string) string { + prefix := "auth" + + hash := GetHash(fmt.Sprintf("%s-%s-%s", prefix, repoType, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + string(repoType) + "-" + + if len(internalRepoName) > 35 { + result += internalRepoName[:35] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalRepositoryTLSSecretName(repoType InternalRepositoryType, internalRepoName string) string { + prefix := "tls" + + hash := GetHash(fmt.Sprintf("%s-%s-%s", prefix, repoType, internalRepoName)) + + var result, postfix string + + result = prefix + "-" + string(repoType) + "-" + + if len(internalRepoName) > 35 { + result += internalRepoName[:35] + postfix = "-" + hash + } else { + result += internalRepoName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetHelmClusterAddonChartName(repoName, addonName string) string { + hash := GetHash(fmt.Sprintf("%s-%s", repoName, addonName)) + + var result, postfix string + + if len(repoName) > 25 { + result += repoName[:25] + postfix = "-" + hash + } else { + result += repoName + } + + if len(addonName) > 25 { + result += "-" + addonName[:25] + postfix = "-" + hash + } else { + result += "-" + addonName + } + + return strings.TrimRight(result, "-") + postfix +} + +func GetInternalHelmChartName(repoName, chartName, chartVersion string) string { + prefix := "addon" + hash := GetHash(fmt.Sprintf("%s-%s-%s-%s", prefix, repoName, chartName, chartVersion)) + + result := prefix + "-" + + if len(repoName) > 25 { + result += repoName[:25] + } else { + result += repoName + } + + if len(chartName) > 25 { + result += "-" + chartName[:25] + } else { + result += "-" + chartName + } + + return strings.TrimRight(result, "-") + "-" + hash +} diff --git a/images/operator-helm-artifact/pkg/utils/repository.go b/images/operator-helm-artifact/pkg/utils/repository.go new file mode 100644 index 0000000..ad6da7c --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/repository.go @@ -0,0 +1,45 @@ +/* +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 utils + +import ( + "fmt" + "net/url" +) + +type InternalRepositoryType string + +const ( + InternalHelmRepository InternalRepositoryType = "helm" + InternalOCIRepository InternalRepositoryType = "oci" +) + +func GetRepositoryType(s string) (InternalRepositoryType, error) { + parsedURL, err := url.Parse(s) + if err != nil { + return "", fmt.Errorf("cannot parse url: %w", err) + } + + switch parsedURL.Scheme { + case "http", "https": + return InternalHelmRepository, nil + case "oci": + return InternalOCIRepository, nil + default: + return "", fmt.Errorf("unsupported repository schema in use: %s", parsedURL.Scheme) + } +} From 5e3ab994d38d875757f3437444f30d73469c0772 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Tue, 3 Mar 2026 13:44:47 +0300 Subject: [PATCH 13/26] feat: add admision and validation webhooks Signed-off-by: Ilya Drey --- api/go.mod | 20 +- api/go.sum | 29 +- api/v1alpha1/helm_cluster_addon.go | 50 +++ .../cmd/operator-helm-module-hooks/main.go | 25 ++ .../operator-helm-module-hooks/register.go | 21 ++ images/hooks/go.mod | 93 +++++ images/hooks/go.sum | 343 ++++++++++++++++++ .../hooks/tls-certificates-controller/hook.go | 40 ++ images/hooks/pkg/settings/certificate.go | 21 ++ images/hooks/pkg/settings/module.go | 23 ++ images/hooks/werf.inc.yaml | 52 +++ .../cmd/operator-helm-controller/main.go | 5 + images/operator-helm-artifact/go.mod | 1 - .../pkg/api/openapi/zz_generated.openapi.go | 17 + .../operator-helm-artifact/pkg/utils/name.go | 18 +- openapi/values.yaml | 34 +- templates/_helpers.tpl | 6 - templates/admision-policy.yaml | 53 +++ .../operator-helm-controller/deployment.yaml | 8 + .../operator-helm-controller/secret-tls.yaml | 12 + .../operator-helm-controller/service.yaml | 19 + .../validation-webhook.yaml | 23 ++ werf.yaml | 8 +- 23 files changed, 881 insertions(+), 40 deletions(-) create mode 100644 images/hooks/cmd/operator-helm-module-hooks/main.go create mode 100644 images/hooks/cmd/operator-helm-module-hooks/register.go create mode 100644 images/hooks/go.mod create mode 100644 images/hooks/go.sum create mode 100644 images/hooks/pkg/hooks/tls-certificates-controller/hook.go create mode 100644 images/hooks/pkg/settings/certificate.go create mode 100644 images/hooks/pkg/settings/module.go create mode 100644 images/hooks/werf.inc.yaml create mode 100644 templates/admision-policy.yaml create mode 100644 templates/operator-helm-controller/secret-tls.yaml create mode 100644 templates/operator-helm-controller/service.yaml create mode 100644 templates/operator-helm-controller/validation-webhook.yaml diff --git a/api/go.mod b/api/go.mod index 52759ce..4eaa1f4 100644 --- a/api/go.mod +++ b/api/go.mod @@ -12,18 +12,25 @@ require ( k8s.io/apiextensions-apiserver v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 + sigs.k8s.io/controller-runtime v0.23.1 ) require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.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/gobuffalo/flect v1.0.3 // indirect + github.com/google/btree v1.1.3 // 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-20250630185457-6e76a2b096b5 // indirect @@ -39,6 +46,10 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/onsi/gomega v1.38.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -48,11 +59,12 @@ require ( golang.org/x/net v0.48.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sync v0.19.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/time v0.9.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.40.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/api/go.sum b/api/go.sum index 0555393..883263f 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,5 +1,7 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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= @@ -7,6 +9,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= @@ -15,6 +19,7 @@ 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/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-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= @@ -27,11 +32,13 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= 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= @@ -41,6 +48,7 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 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= @@ -48,6 +56,7 @@ 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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -73,6 +82,10 @@ github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -95,6 +108,8 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -109,20 +124,17 @@ golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/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/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= -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/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -156,6 +168,7 @@ k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= sigs.k8s.io/controller-tools v0.17.2 h1:jNFOKps8WnaRKZU2R+4vRCHnXyJanVmXBWqkuUPFyFg= sigs.k8s.io/controller-tools v0.17.2/go.mod h1:4q5tZG2JniS5M5bkiXY2/potOiXyhoZVw/U48vLkXk0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index 0198456..ac689f6 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -17,8 +17,14 @@ limitations under the License. package v1alpha1 import ( + "context" + "fmt" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) const ( @@ -46,6 +52,12 @@ type HelmClusterAddon struct { Status HelmClusterAddonStatus `json:"status,omitempty"` } +func (r *HelmClusterAddon) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr, r). + WithValidator(&HelmClusterAddonValidator{Client: mgr.GetClient()}). + Complete() +} + type HelmClusterAddonSpec struct { Chart HelmClusterAddonChartRef `json:"chart"` // Values holds the values for this HelmClusterAddon release. @@ -110,3 +122,41 @@ type HelmClusterAddonMaintanace string const ( NoResourceReconciliation HelmClusterAddonMaintanace = "NoResourceReconciliation" ) + +// +k8s:deepcopy-gen=false +type HelmClusterAddonValidator struct { + Client client.Client +} + +func (v *HelmClusterAddonValidator) ValidateCreate(ctx context.Context, addon *HelmClusterAddon) (admission.Warnings, error) { + return nil, v.checkUniqueness(ctx, addon) +} + +func (v *HelmClusterAddonValidator) ValidateUpdate(ctx context.Context, _, newObj *HelmClusterAddon) (admission.Warnings, error) { + return nil, v.checkUniqueness(ctx, newObj) +} + +func (v *HelmClusterAddonValidator) ValidateDelete(_ context.Context, _ *HelmClusterAddon) (admission.Warnings, error) { + return nil, nil +} + +func (v *HelmClusterAddonValidator) checkUniqueness(ctx context.Context, addon *HelmClusterAddon) error { + list := &HelmClusterAddonList{} + + if err := v.Client.List(ctx, list); err != nil { + return err + } + + for _, existing := range list.Items { + if existing.Name != addon.Name && + existing.Spec.Chart.HelmClusterAddonRepository == addon.Spec.Chart.HelmClusterAddonRepository && + existing.Spec.Chart.HelmClusterAddonChartName == addon.Spec.Chart.HelmClusterAddonChartName { + return fmt.Errorf( + "chart %s is already used by helmclusteraddon/%s", + addon.Spec.Chart.HelmClusterAddonChartName, existing.Name, + ) + } + } + + return nil +} diff --git a/images/hooks/cmd/operator-helm-module-hooks/main.go b/images/hooks/cmd/operator-helm-module-hooks/main.go new file mode 100644 index 0000000..e0fcf75 --- /dev/null +++ b/images/hooks/cmd/operator-helm-module-hooks/main.go @@ -0,0 +1,25 @@ +/* +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 main + +import ( + "github.com/deckhouse/module-sdk/pkg/app" +) + +func main() { + app.Run() +} diff --git a/images/hooks/cmd/operator-helm-module-hooks/register.go b/images/hooks/cmd/operator-helm-module-hooks/register.go new file mode 100644 index 0000000..8c21530 --- /dev/null +++ b/images/hooks/cmd/operator-helm-module-hooks/register.go @@ -0,0 +1,21 @@ +/* +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 main + +import ( + _ "hooks/pkg/hooks/tls-certificates-controller" +) diff --git a/images/hooks/go.mod b/images/hooks/go.mod new file mode 100644 index 0000000..c2c7496 --- /dev/null +++ b/images/hooks/go.mod @@ -0,0 +1,93 @@ +module hooks + +go 1.25.0 + +require github.com/deckhouse/module-sdk v0.10.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/cloudflare/cfssl v1.6.5 // 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.2.0 // 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/google/btree v1.1.3 // indirect + github.com/google/certificate-transparency-go v1.1.7 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jmoiron/sqlx v1.3.5 // 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/pelletier/go-toml v1.9.3 // 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.22.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.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.7.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/weppos/publicsuffix-go v0.30.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect + github.com/zmap/zlint/v3 v3.5.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.38.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.30.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.9.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // 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/api v0.33.8 // indirect + k8s.io/apiextensions-apiserver v0.33.8 // indirect + k8s.io/apimachinery v0.33.8 // indirect + k8s.io/client-go v0.33.8 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // 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/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect + sigs.k8s.io/yaml v1.4.0 // indirect +) diff --git a/images/hooks/go.sum b/images/hooks/go.sum new file mode 100644 index 0000000..e9f18d4 --- /dev/null +++ b/images/hooks/go.sum @@ -0,0 +1,343 @@ +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/cloudflare/cfssl v1.6.5 h1:46zpNkm6dlNkMZH/wMW22ejih6gIaJbzL2du6vD7ZeI= +github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZVaD+Taky4= +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.2.0 h1:6tmZQLwNb1o/hP1gzJQBjcwfA/bubbgObovXzxq+Exo= +github.com/deckhouse/deckhouse/pkg/log v0.2.0/go.mod h1:pbAxTSDcPmwyl3wwKDcEB3qdxHnRxqTV+J0K+sha8bw= +github.com/deckhouse/module-sdk v0.10.0 h1:VPhYvMVQ3pT32I2WL1ITtQyrYdpiUR0RocLw7S4TfNg= +github.com/deckhouse/module-sdk v0.10.0/go.mod h1:Z1jfmd0fICoYww0daMijWAU+OZTxeJUXfMciKKuYAYA= +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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= +github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +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/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +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/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= +github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= +github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= +github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +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/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +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/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= +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/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +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/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +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.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= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/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.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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.1.1/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.7.0 h1:SIisUvcEL+Vpa9/kmQDy1W3AwV2XVGad83sgZmXLlb0= +github.com/sylabs/oci-tools v0.7.0/go.mod h1:Ry6ngChflh20WPq6mLvCKSw2OTd9iDB5aR8OQzeq4hM= +github.com/sylabs/sif/v2 v2.15.0 h1:Nv0tzksFnoQiQ2eUwpAis9nVqEu4c3RcNSxX8P3Cecw= +github.com/sylabs/sif/v2 v2.15.0/go.mod h1:X1H7eaPz6BAxA84POMESXoXfTqgAnLQkujyF/CQFWTc= +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/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.13.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= +github.com/weppos/publicsuffix-go v0.30.0 h1:QHPZ2GRu/YE7cvejH9iyavPOkVCB4dNxp2ZvtT+vQLY= +github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnnn+btVN8uWPMyXAY= +github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE= +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= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= +github.com/zmap/zcertificate v0.0.0-20180516150559-0e3d58b1bac4/go.mod h1:5iU54tB79AMBcySS0R2XIyZBAVmeHranShAFELYx7is= +github.com/zmap/zcertificate v0.0.1/go.mod h1:q0dlN54Jm4NVSSuzisusQY0hqDWvu92C+TWveAxiVWk= +github.com/zmap/zcrypto v0.0.0-20201128221613-3719af1573cf/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20201211161100-e54a5822fb7e/go.mod h1:aPM7r+JOkfL+9qSB4KbYjtoEzJqUK50EXkkJabeNJDQ= +github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 h1:DZH5n7L3L8RxKdSyJHZt7WePgwdhHnPhQFdQSJaHF+o= +github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300/go.mod h1:mOd4yUMgn2fe2nV9KXsa9AyQBFZGzygVPovsZR+Rl5w= +github.com/zmap/zlint/v3 v3.0.0/go.mod h1:paGwFySdHIBEMJ61YjoqT4h7Ge+fdYG4sUQhnTb1lJ8= +github.com/zmap/zlint/v3 v3.5.0 h1:Eh2B5t6VKgVH0DFmTwOqE50POvyDhUaU9T2mJOe1vfQ= +github.com/zmap/zlint/v3 v3.5.0/go.mod h1:JkNSrsDJ8F4VRtBZcYUQSvnWFL7utcjDIn+FE64mlBI= +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-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +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.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +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-20200202094626-16171245cfb2/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.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= +golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/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-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201126233918-771906719818/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/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.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +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.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +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.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/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-20180628173108-788fd7840127/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.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +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.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= +gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +k8s.io/api v0.33.8 h1:IPju/eyOsfnIN4EVR9U5qrxe1CpNBZi+JtvvfKLvq6s= +k8s.io/api v0.33.8/go.mod h1:POmJWNXzip1LpvMhSWpjWmbzgyoa1Rt0FRxMwe2s7QA= +k8s.io/apiextensions-apiserver v0.33.8 h1:7zHBQBsZYyZu2Tay++lIUdjexxCxcr5TinL11y/d3HA= +k8s.io/apiextensions-apiserver v0.33.8/go.mod h1:phLEQv7OSxpMbDr7dZYSd+WNzKCQ8TdNJU5iCHyo1OM= +k8s.io/apimachinery v0.33.8 h1:aiUOauowBiUz4IFJktqxAMSHx4AYmmqB1Yq4QgnaZ6E= +k8s.io/apimachinery v0.33.8/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/client-go v0.33.8 h1:RX5exRQynxnGB5LJdviBaZXSR3vn3LWp3/bn87+wl8w= +k8s.io/client-go v0.33.8/go.mod h1:MA2z0/7JdSqTx9BAfvl+Vng+maMGRcn3ZkURrx5rKUs= +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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= +k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +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/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= +sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +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/images/hooks/pkg/hooks/tls-certificates-controller/hook.go b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go new file mode 100644 index 0000000..0265681 --- /dev/null +++ b/images/hooks/pkg/hooks/tls-certificates-controller/hook.go @@ -0,0 +1,40 @@ +/* +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 tls_certificates_controller + +import ( + "fmt" + "hooks/pkg/settings" + + tlscertificate "github.com/deckhouse/module-sdk/common-hooks/tls-certificate" +) + +var _ = tlscertificate.RegisterInternalTLSHookEM(tlscertificate.GenSelfSignedTLSHookConf{ + CN: settings.ControllerCertCN, + TLSSecretName: "operator-helm-controller-tls", + Namespace: settings.ModuleNamespace, + SANs: tlscertificate.DefaultSANs([]string{ + "localhost", + "127.0.0.1", + settings.ControllerCertCN, + fmt.Sprintf("%s.%s", settings.ControllerCertCN, settings.ModuleNamespace), + fmt.Sprintf("%s.%s.svc", settings.ControllerCertCN, settings.ModuleNamespace), + }), + + FullValuesPathPrefix: fmt.Sprintf("%s.internal.controller.cert", settings.ModuleName), + CommonCAValuesPath: fmt.Sprintf("%s.internal.rootCA", settings.ModuleName), +}) diff --git a/images/hooks/pkg/settings/certificate.go b/images/hooks/pkg/settings/certificate.go new file mode 100644 index 0000000..5d08591 --- /dev/null +++ b/images/hooks/pkg/settings/certificate.go @@ -0,0 +1,21 @@ +/* +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 settings + +const ( + ControllerCertCN string = "operator-helm-controller" +) diff --git a/images/hooks/pkg/settings/module.go b/images/hooks/pkg/settings/module.go new file mode 100644 index 0000000..095d744 --- /dev/null +++ b/images/hooks/pkg/settings/module.go @@ -0,0 +1,23 @@ +/* +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 settings + +// Essential module constants. +const ( + ModuleNamespace string = "d8-operator-helm" + ModuleName string = "operatorHelm" +) diff --git a/images/hooks/werf.inc.yaml b/images/hooks/werf.inc.yaml new file mode 100644 index 0000000..ade8485 --- /dev/null +++ b/images/hooks/werf.inc.yaml @@ -0,0 +1,52 @@ +--- +image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact +final: false +fromImage: builder/src +git: + - add: {{ .ModuleDir }}/images/{{ .ImageName }} + to: /src/images/hooks + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/images/operator-helm-artifact + to: /src/images/operator-helm-artifact + stageDependencies: + install: + - "**/*" + - add: {{ .ModuleDir }}/api + to: /src/api + stageDependencies: + install: + - "**/*" +shell: + install: + - cd /src +--- +image: {{ .ModuleNamePrefix }}go-hooks-artifact +final: false +fromImage: {{ eq $.SVACE_ENABLED "false" | ternary "builder/golang-bookworm-1.25" "builder/golang-alt-svace-1.25" }} +import: + - image: {{ .ModuleNamePrefix }}{{ .ImageName }}-src-artifact + add: /src + to: /app + before: install +mount: + - fromPath: ~/go-pkg-cache + to: /go/pkg +secrets: + - id: GOPROXY + value: {{ .GOPROXY }} +shell: + install: + - export GOPROXY=$(cat /run/secrets/GOPROXY) + - cd /app/images/hooks + - go mod download + setup: + - cd /app/images/hooks + - | + export GOOS=linux + export GOARCH=amd64 + export CGO_ENABLED=0 + export TAGS="{{ printf "-tags %s" .MODULE_EDITION }}" + {{- $_ := set $ "ProjectName" (list $.ImageName "hooks" | join "/") }} + {{- include "image-build.build" (set $ "BuildCommand" `go build -ldflags="-s -w" $TAGS -a -o /go-hooks/operator-helm-module-hooks ./cmd/operator-helm-module-hooks`) | nindent 6 }} diff --git a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go index c7babc3..9ccc22d 100644 --- a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -88,6 +88,11 @@ func main() { os.Exit(1) } + if err = (&helmv1alpha1.HelmClusterAddon{}).SetupWebhookWithManager(mgr); err != nil { + logger.Error(err, "unable to create webhook", "webhook", "HelmClusterAddon") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { logger.Error(err, "unable to set up health check") os.Exit(1) diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod index 2eea7c8..c4843c5 100644 --- a/images/operator-helm-artifact/go.mod +++ b/images/operator-helm-artifact/go.mod @@ -23,7 +23,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index fd385b3..8bb8180 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -45,6 +45,7 @@ func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenA "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref), "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonValidator": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref), v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), @@ -713,6 +714,22 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref comm } } +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "Client": { + SchemaProps: spec.SchemaProps{}, + }, + }, + Required: []string{"Client"}, + }, + }, + } +} + func schema_pkg_apis_meta_v1_APIGroup(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ diff --git a/images/operator-helm-artifact/pkg/utils/name.go b/images/operator-helm-artifact/pkg/utils/name.go index a143709..94f9031 100644 --- a/images/operator-helm-artifact/pkg/utils/name.go +++ b/images/operator-helm-artifact/pkg/utils/name.go @@ -27,7 +27,7 @@ func GetHash(s string) string { _, _ = h.Write([]byte(s)) - return fmt.Sprintf("%x", h.Sum32())[:5] + return fmt.Sprintf("%x", h.Sum32()) } func GetInternalRepositoryAuthSecretName(repoType InternalRepositoryType, internalRepoName string) string { @@ -73,15 +73,15 @@ func GetHelmClusterAddonChartName(repoName, addonName string) string { var result, postfix string - if len(repoName) > 25 { - result += repoName[:25] + if len(repoName) > 20 { + result += repoName[:20] postfix = "-" + hash } else { result += repoName } - if len(addonName) > 25 { - result += "-" + addonName[:25] + if len(addonName) > 20 { + result += "-" + addonName[:20] postfix = "-" + hash } else { result += "-" + addonName @@ -96,14 +96,14 @@ func GetInternalHelmChartName(repoName, chartName, chartVersion string) string { result := prefix + "-" - if len(repoName) > 25 { - result += repoName[:25] + if len(repoName) > 20 { + result += repoName[:20] } else { result += repoName } - if len(chartName) > 25 { - result += "-" + chartName[:25] + if len(chartName) > 20 { + result += "-" + chartName[:20] } else { result += "-" + chartName } diff --git a/openapi/values.yaml b/openapi/values.yaml index 438606f..47187f8 100644 --- a/openapi/values.yaml +++ b/openapi/values.yaml @@ -6,15 +6,33 @@ properties: type: object default: {} properties: - moduleConfig: - type: object - additionalProperties: true - moduleConfigValidation: + controller: type: object + default: {} properties: - error: - type: string - moduleState: + cert: + type: object + default: {} + properties: + ca: + type: string + default: "" + crt: + type: string + default: "" + key: + type: string + default: "" + rootCA: type: object default: {} - additionalProperties: true + properties: + ca: + type: string + default: "" + crt: + type: string + default: "" + key: + type: string + default: "" \ No newline at end of file diff --git a/templates/_helpers.tpl b/templates/_helpers.tpl index 15d4c6b..38cd5d2 100644 --- a/templates/_helpers.tpl +++ b/templates/_helpers.tpl @@ -3,12 +3,6 @@ {{- dig "logLevel" "" .Values.operatorHelm -}} {{- end }} -{{- define "hasValidModuleConfig" -}} -{{- if (hasKey .Values.operatorHelm.internal "moduleConfig" ) -}} -true -{{- end }} -{{- end }} - {{- define "priorityClassName" -}} system-cluster-critical {{- end }} diff --git a/templates/admision-policy.yaml b/templates/admision-policy.yaml new file mode 100644 index 0000000..5ca4b4d --- /dev/null +++ b/templates/admision-policy.yaml @@ -0,0 +1,53 @@ +{{- $kubeVersion := .Values.global.discovery.kubernetesVersion }} +{{- $apiVersion := "" }} +{{- if semverCompare ">=1.30.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1" }} +{{- else if semverCompare ">=1.28.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1beta1" }} +{{- else if semverCompare ">=1.26.0" $kubeVersion }} +{{- $apiVersion = "admissionregistration.k8s.io/v1alpha1" }} +{{- end }} + +{{- if $apiVersion }} +apiVersion: {{ $apiVersion }} +kind: ValidatingAdmissionPolicy +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-restricted-access-policy +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - "helm.internal.operator-helm.deckhouse.io" + - "source.internal.operator-helm.deckhouse.io" + apiVersions: ["*"] + operations: + - "CREATE" + - "UPDATE" + - "DELETE" + resources: ["*"] + validations: + - expression: | + request.userInfo.username.startsWith("system:serviceaccount:kube-system:") || + request.userInfo.username.startsWith("system:serviceaccount:d8-system:") || + request.userInfo.username in [ + "system:serviceaccount:d8-operator-helm:operator-helm-controller", + "system:serviceaccount:d8-operator-helm:nelm-source-controller", + "system:serviceaccount:d8-operator-helm:helm-controller", + ] + message: "Operation forbidden for this user." +--- +apiVersion: {{ $apiVersion }} +kind: ValidatingAdmissionPolicyBinding +metadata: + {{- include "helm_lib_module_labels" (list .) | nindent 2 }} + name: operator-helm-restricted-access-policy-binding +spec: + policyName: operator-helm-restricted-access-policy + validationActions: + - "Deny" + matchResources: + namespaceSelector: {} + objectSelector: {} +{{- end }} \ No newline at end of file diff --git a/templates/operator-helm-controller/deployment.yaml b/templates/operator-helm-controller/deployment.yaml index 6cfad87..7e01c31 100644 --- a/templates/operator-helm-controller/deployment.yaml +++ b/templates/operator-helm-controller/deployment.yaml @@ -84,8 +84,13 @@ spec: - --metrics-bind-address=:8080 - --health-probe-bind-address=:9440 volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: admission-webhook-secret {{- include "kube_api_rewriter.kubeconfig_volume_mount" . | nindent 12 }} ports: + - containerPort: 9443 + name: controller + protocol: TCP - containerPort: 8080 name: metrics protocol: TCP @@ -130,4 +135,7 @@ spec: {{- include "helm_lib_tolerations" (tuple . "system") | nindent 6 }} {{- include "helm_lib_pod_anti_affinity_for_ha" (list . (dict "app" "operator-helm-controller")) | nindent 6 }} volumes: + - name: admission-webhook-secret + secret: + secretName: operator-helm-controller-tls {{- include "kube_api_rewriter.kubeconfig_volume" . | nindent 8 }} diff --git a/templates/operator-helm-controller/secret-tls.yaml b/templates/operator-helm-controller/secret-tls.yaml new file mode 100644 index 0000000..b2f69a1 --- /dev/null +++ b/templates/operator-helm-controller/secret-tls.yaml @@ -0,0 +1,12 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: operator-helm-controller-tls + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +type: kubernetes.io/tls +data: + ca.crt: {{ .Values.operatorHelm.internal.controller.cert.ca | b64enc }} + tls.crt: {{ .Values.operatorHelm.internal.controller.cert.crt | b64enc }} + tls.key: {{ .Values.operatorHelm.internal.controller.cert.key | b64enc }} diff --git a/templates/operator-helm-controller/service.yaml b/templates/operator-helm-controller/service.yaml new file mode 100644 index 0000000..b1356b6 --- /dev/null +++ b/templates/operator-helm-controller/service.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: v1 +kind: Service +metadata: + name: operator-helm-controller + namespace: d8-{{ .Chart.Name }} + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} +spec: + ports: + - name: admission-webhook + port: 443 + targetPort: controller + protocol: TCP + - name: controller + port: 9443 + targetPort: controller + protocol: TCP + selector: + app: operator-helm-controller diff --git a/templates/operator-helm-controller/validation-webhook.yaml b/templates/operator-helm-controller/validation-webhook.yaml new file mode 100644 index 0000000..b1e3ed9 --- /dev/null +++ b/templates/operator-helm-controller/validation-webhook.yaml @@ -0,0 +1,23 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + {{- include "helm_lib_module_labels" (list . (dict "app" "operator-helm-controller")) | nindent 2 }} + name: "operator-helm-controller-admission-webhook" +webhooks: + - name: "helmclusteraddons.operator-helm-controller.validate.d8-operator-helm" + rules: + - apiGroups: ["helm.deckhouse.io"] + apiVersions: ["v1alpha1"] + operations: ["CREATE", "UPDATE"] + resources: ["helmclusteraddons"] + scope: "Cluster" + clientConfig: + service: + namespace: d8-{{ .Chart.Name }} + name: operator-helm-controller + path: /validate-helm-deckhouse-io-v1alpha1-helmclusteraddon + port: 443 + caBundle: | + {{ .Values.operatorHelm.internal.controller.cert.ca | b64enc }} + admissionReviewVersions: ["v1"] + sideEffects: None diff --git a/werf.yaml b/werf.yaml index 160b16d..5b2d5a1 100644 --- a/werf.yaml +++ b/werf.yaml @@ -77,10 +77,10 @@ import: after: setup includePaths: - images_digests.json -# - image: go-hooks-artifact -# add: /go-hooks -# to: /prep-bundle/hooks/go -# after: setup +- image: go-hooks-artifact + add: /go-hooks + to: /prep-bundle/hooks/go + after: setup git: - add: / to: /prep-bundle From ffc29a20a854aac2c9860a78c0e354261783b363 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Tue, 3 Mar 2026 21:16:50 +0300 Subject: [PATCH 14/26] feat: re-work HelmClusterAddonChart reconcile Signed-off-by: Ilya Drey --- .../cmd/operator-helm-controller/main.go | 6 + .../controller/helmclusteraddon/constants.go | 2 +- .../controller/helmclusteraddon/controller.go | 2 +- .../controller/helmclusteraddon/reconciler.go | 149 ++++++++++-------- .../helmclusteraddonchart/constants.go | 29 ++++ .../helmclusteraddonchart/controller.go | 79 ++++++++++ .../helmclusteraddonchart/reconciler.go | 76 +++++++++ .../helmclusteraddonrepository/constants.go | 2 +- .../helmclusteraddonrepository/controller.go | 2 +- .../helmclusteraddonrepository/reconciler.go | 14 +- 10 files changed, 285 insertions(+), 76 deletions(-) create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go create mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go diff --git a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go index 9ccc22d..9c5e1f5 100644 --- a/images/operator-helm-artifact/cmd/operator-helm-controller/main.go +++ b/images/operator-helm-artifact/cmd/operator-helm-controller/main.go @@ -20,6 +20,7 @@ import ( "flag" "os" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonchart" helmv2 "github.com/werf/3p-helm-controller/api/v2" "k8s.io/apimachinery/pkg/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" @@ -93,6 +94,11 @@ func main() { os.Exit(1) } + if err := helmclusteraddonchart.SetupWithManager(mgr); err != nil { + logger.Error(err, "unable to setup HelmClusterAddonChart controller") + os.Exit(1) + } + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { logger.Error(err, "unable to set up health check") os.Exit(1) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go index ed9b07d..d9df12c 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -48,5 +48,5 @@ const ( LabelManagedBy = "helm.deckhouse.io/managed-by" LabelManagedByValue = "operator-helm" - LabelSourceName = "helm.deckhouse.io/cluster-addon-name" + LabelSourceName = "helm.deckhouse.io/cluster-addon" ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go index 32720fd..eedfbee 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go @@ -68,7 +68,7 @@ func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Re sourceName := labels[LabelSourceName] if sourceName == "" { - logger.Info("Internal repository resource missing source-name label, skipping", + logger.Info("Internal repository resource missing cluster-addon label, skipping", "name", obj.GetName(), "namespace", obj.GetNamespace()) return nil diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index b7f6aa5..c4fe614 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -20,7 +20,9 @@ import ( "context" "errors" "fmt" + "time" + "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonchart" "github.com/deckhouse/operator-helm/pkg/utils" helmv2 "github.com/werf/3p-helm-controller/api/v2" sourcev1 "github.com/werf/nelm-source-controller/api/v1" @@ -86,41 +88,17 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return r.reconcileInternalRelease(ctx, &release) } -func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { +func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { logger := log.FromContext(ctx) - var addonChart helmv1alpha1.HelmClusterAddonChart - - if err := r.Client.Get( - ctx, - types.NamespacedName{ - Name: utils.GetHelmClusterAddonChartName(release.Spec.Chart.HelmClusterAddonRepository, - release.Spec.Chart.HelmClusterAddonRepository), - }, - &addonChart, - ); err != nil { - if apierrors.IsNotFound(err) { - return fmt.Errorf("addon chart not found: %w", err) - } - - return fmt.Errorf("getting HelmClusterAddonChart: %w", err) - } - - // TODO: implement logic depending on pulled flag in the HelmClusterAddonChart - //for _, chartInfo := range addonChart.Status.Versions { - // if chartInfo.Pulled && chartInfo.Version == release.Spec.Chart.Version { - // - // } - //} - repoType, _ := utils.GetRepositoryType(repo.Spec.URL) existing := &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ Name: utils.GetInternalHelmChartName( - release.Name, - release.Spec.Chart.HelmClusterAddonChartName, - release.Spec.Chart.Version), + addon.Name, + addon.Spec.Chart.HelmClusterAddonChartName, + addon.Spec.Chart.Version), Namespace: TargetNamespace, }, } @@ -131,21 +109,23 @@ func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, release *he } existing.Labels[LabelManagedBy] = LabelManagedByValue - existing.Labels[LabelSourceName] = release.Name + existing.Labels[LabelSourceName] = addon.Name + existing.Labels[helmclusteraddonchart.LabelSourceName] = utils.GetHelmClusterAddonChartName( + repo.Name, addon.Spec.Chart.HelmClusterAddonChartName) - existing.Spec.Chart = release.Spec.Chart.HelmClusterAddonChartName - existing.Spec.Version = release.Spec.Chart.Version + existing.Spec.Chart = addon.Spec.Chart.HelmClusterAddonChartName + existing.Spec.Version = addon.Spec.Chart.Version switch repoType { case utils.InternalHelmRepository: existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ Kind: sourcev1.HelmRepositoryKind, - Name: release.Spec.Chart.HelmClusterAddonRepository, + Name: addon.Spec.Chart.HelmClusterAddonRepository, } case utils.InternalOCIRepository: existing.Spec.SourceRef = sourcev1.LocalHelmChartSourceReference{ Kind: sourcev1.OCIRepositoryKind, - Name: release.Spec.Chart.HelmClusterAddonRepository, + Name: addon.Spec.Chart.HelmClusterAddonRepository, } default: return fmt.Errorf("invalid repository type: %s", repoType) @@ -158,18 +138,47 @@ func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, release *he } if op != controllerutil.OperationResultNone { - logger.Info("Successfully reconciled internal helm chart", "operation", op, "repository", repo.Name, "chart", release.Spec.Chart.HelmClusterAddonChartName) + logger.Info("Successfully reconciled internal helm chart", "operation", op, "repository", repo.Name, "chart", addon.Spec.Chart.HelmClusterAddonChartName) } return nil } -func (r *Reconciler) reconcileInternalRelease(ctx context.Context, release *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { +func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { logger := log.FromContext(ctx) + var addonChart helmv1alpha1.HelmClusterAddonChart + + if err := r.Client.Get( + ctx, + types.NamespacedName{ + Name: utils.GetHelmClusterAddonChartName(addon.Spec.Chart.HelmClusterAddonRepository, + addon.Spec.Chart.HelmClusterAddonRepository), + }, + &addonChart, + ); err != nil { + if apierrors.IsNotFound(err) { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("addon chart not found: %w", err), ReasonMirrorFailed) + } + + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("getting HelmClusterAddonChart: %w", err), ReasonMirrorFailed) + } + + var chartPulled bool + for _, chartInfo := range addonChart.Status.Versions { + if addon.Spec.Chart.Version == chartInfo.Version { + chartPulled = chartInfo.Pulled + } + } + + if !chartPulled { + // TODO: need to reflect the current state in the HelmClusterAddon status. + return reconcile.Result{RequeueAfter: 10 * time.Second}, nil + } + existing := &helmv2.HelmRelease{ ObjectMeta: metav1.ObjectMeta{ - Name: release.Name, + Name: addon.Name, Namespace: TargetNamespace, }, } @@ -180,40 +189,40 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, release *helm } existing.Labels[LabelManagedBy] = LabelManagedByValue - existing.Labels[LabelSourceName] = release.Name + existing.Labels[LabelSourceName] = addon.Name - existing.Spec.ReleaseName = release.Name - existing.Spec.TargetNamespace = release.Spec.Namespace - existing.Spec.Values = release.Spec.Values + existing.Spec.ReleaseName = addon.Name + existing.Spec.TargetNamespace = addon.Spec.Namespace + existing.Spec.Values = addon.Spec.Values existing.Spec.DriftDetection = &helmv2.DriftDetection{ Mode: helmv2.DriftDetectionWarn, } - if release.Spec.Maintanace != "" { + if addon.Spec.Maintanace != "" { existing.Spec.DriftDetection.Mode = helmv2.DriftDetectionEnabled } existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ Kind: sourcev1.HelmChartKind, Name: utils.GetInternalHelmChartName( - release.Name, - release.Spec.Chart.HelmClusterAddonChartName, - release.Spec.Chart.Version), + addon.Name, + addon.Spec.Chart.HelmClusterAddonChartName, + addon.Spec.Chart.Version), Namespace: TargetNamespace, } return nil }) if err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("reconcile internal helm release: %w", err), ReasonMirrorFailed) + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("reconcile internal helm release: %w", err), ReasonMirrorFailed) } if op != controllerutil.OperationResultNone { - logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", release.Spec.Chart.HelmClusterAddonChartName) + logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", addon.Spec.Chart.HelmClusterAddonChartName) } - return r.updateSuccessStatus(ctx, release, existing.Status.Conditions) + return r.updateSuccessStatus(ctx, addon, existing.Status.Conditions) } // ensureResourceDeleted safely deletes an object if it exists. @@ -234,24 +243,24 @@ func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace } // reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. -func (r *Reconciler) reconcileDelete(ctx context.Context, release *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { - logger := log.FromContext(ctx).WithValues("helmclusteraddon", release.Name) +func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddon", addon.Name) - if !controllerutil.ContainsFinalizer(release, FinalizerName) { + if !controllerutil.ContainsFinalizer(addon, FinalizerName) { return reconcile.Result{}, nil } - if err := r.ensureResourceDeleted(ctx, release.Name, TargetNamespace, &helmv2.HelmRelease{}); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("deleting internal helm release: %w", err), ReasonCleanupFailed) + if err := r.ensureResourceDeleted(ctx, addon.Name, TargetNamespace, &helmv2.HelmRelease{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm release: %w", err), ReasonCleanupFailed) } - if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(release.Spec.Chart.HelmClusterAddonRepository, release.Spec.Chart.HelmClusterAddonChartName, release.Spec.Chart.Version), TargetNamespace, &sourcev1.HelmChart{}); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, release, fmt.Errorf("deleting internal helm chart: %w", err), ReasonCleanupFailed) + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(addon.Spec.Chart.HelmClusterAddonRepository, addon.Spec.Chart.HelmClusterAddonChartName, addon.Spec.Chart.Version), TargetNamespace, &sourcev1.HelmChart{}); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm chart: %w", err), ReasonCleanupFailed) } - controllerutil.RemoveFinalizer(release, FinalizerName) + controllerutil.RemoveFinalizer(addon, FinalizerName) - if err := r.Client.Update(ctx, release); err != nil { + if err := r.Client.Update(ctx, addon); err != nil { return reconcile.Result{}, fmt.Errorf("removing finalizer: %w", err) } @@ -261,12 +270,12 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, release *helmv1alpha1. } // patchStatusError is a helper to safely patch a failure condition onto the cluster resource. -func (r *Reconciler) patchStatusError(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { - base := release.DeepCopy() +func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { + base := addon.DeepCopy() - r.setCondition(release, metav1.ConditionFalse, reason, reconcileErr.Error()) + r.setCondition(addon, metav1.ConditionFalse, reason, reconcileErr.Error()) - if patchErr := r.Client.Status().Patch(ctx, release, client.MergeFrom(base)); patchErr != nil { + if patchErr := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); patchErr != nil { return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) } @@ -274,13 +283,13 @@ func (r *Reconciler) patchStatusError(ctx context.Context, release *helmv1alpha1 } // updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. -func (r *Reconciler) updateSuccessStatus(ctx context.Context, release *helmv1alpha1.HelmClusterAddon, internalConditions []metav1.Condition) (reconcile.Result, error) { - base := release.DeepCopy() +func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalConditions []metav1.Condition) (reconcile.Result, error) { + base := addon.DeepCopy() - release.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) - release.Status.ObservedGeneration = release.Generation + addon.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) + addon.Status.ObservedGeneration = addon.Generation - if err := r.Client.Status().Patch(ctx, release, client.MergeFrom(base)); err != nil { + if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) } @@ -288,7 +297,7 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, release *helmv1alp } // setCondition is a helper to set a single Ready condition on the cluster resource. -func (r *Reconciler) setCondition(release *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string) { +func (r *Reconciler) setCondition(addon *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string) { now := metav1.Now() newCond := metav1.Condition{ @@ -297,21 +306,21 @@ func (r *Reconciler) setCondition(release *helmv1alpha1.HelmClusterAddon, status Reason: reason, Message: message, LastTransitionTime: now, - ObservedGeneration: release.Generation, + ObservedGeneration: addon.Generation, } - for i, c := range release.Status.Conditions { + for i, c := range addon.Status.Conditions { if c.Type == ConditionTypeReady { // Only update LastTransitionTime if status actually changed. if c.Status == status { newCond.LastTransitionTime = c.LastTransitionTime } - release.Status.Conditions[i] = newCond + addon.Status.Conditions[i] = newCond return } } - release.Status.Conditions = append(release.Status.Conditions, newCond) + addon.Status.Conditions = append(addon.Status.Conditions, newCond) } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go new file mode 100644 index 0000000..fba4b3e --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go @@ -0,0 +1,29 @@ +/* +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 helmclusteraddonchart + +const ( + // ControllerName is the name of this controller, used for leader election and logging. + ControllerName = "helmclusteraddonchart-controller" + + // TargetNamespace is the namespace where internal customer resources are created. + TargetNamespace = "d8-operator-helm" + + LabelManagedBy = "helm.deckhouse.io/managed-by" + LabelManagedByValue = "operator-helm" + LabelSourceName = "helm.deckhouse.io/cluster-addon-chart" +) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go new file mode 100644 index 0000000..1a2a5a9 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go @@ -0,0 +1,79 @@ +/* +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 helmclusteraddonchart + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" +) + +func SetupWithManager(mgr ctrl.Manager) error { + r := &Reconciler{ + Client: mgr.GetClient(), + } + + return ctrl.NewControllerManagedBy(mgr). + Named(ControllerName). + For(&helmv1alpha1.HelmClusterAddonChart{}). + Watches( + &sourcev1.HelmChart{}, + handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), + ). + Complete(r) +} + +func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + if obj.GetNamespace() != TargetNamespace { + return nil + } + + labels := obj.GetLabels() + if labels[LabelManagedBy] != LabelManagedByValue { + return nil + } + + sourceName := labels[LabelSourceName] + if sourceName == "" { + logger.Info("Internal repository resource missing cluster-addon-chart label, skipping", + "name", obj.GetName(), "namespace", obj.GetNamespace()) + + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: sourceName, + Namespace: "", + }, + }, + } +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go new file mode 100644 index 0000000..43a1e06 --- /dev/null +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go @@ -0,0 +1,76 @@ +/* +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 helmclusteraddonchart + +import ( + "context" + "fmt" + + sourcev1 "github.com/werf/nelm-source-controller/api/v1" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" +) + +type Reconciler struct { + Client client.Client +} + +func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonchart", req.Name) + + chart := &helmv1alpha1.HelmClusterAddonChart{} + if err := r.Client.Get(ctx, req.NamespacedName, chart); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to get HelmClusterAddonChart: %w", client.IgnoreNotFound(err)) + } + + internalCharts := &sourcev1.HelmChartList{} + if err := r.Client.List(ctx, internalCharts, client.InNamespace(TargetNamespace)); err != nil { + return ctrl.Result{}, fmt.Errorf("failed to list internal helm chart list: %w", err) + } + + needsUpdate := false + for i, v := range chart.Status.Versions { + found := false + for _, child := range internalCharts.Items { + if child.Spec.Version == v.Version && child.Status.Artifact != nil { + found = true + break + } + } + + if chart.Status.Versions[i].Pulled != found { + chart.Status.Versions[i].Pulled = found + needsUpdate = true + } + } + + if needsUpdate { + if err := r.Client.Status().Update(ctx, chart); err != nil { + if client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) + } + } + + logger.Info("HelmClusterAddonChart successfully reconciled") + } + + return reconcile.Result{}, nil +} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index 4a99aed..f04551d 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -55,7 +55,7 @@ const ( LabelManagedByValue = "operator-helm" // LabelSourceName stores the name of the source HelmClusterAddonRepository. - LabelSourceName = "helm.deckhouse.io/cluster-addon-repository-name" + LabelSourceName = "helm.deckhouse.io/cluster-addon-repository" // DefaultInterval is the default reconciliation interval for the internal repository. DefaultInterval = 5 * time.Second diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go index 5403ed2..5e51729 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go @@ -73,7 +73,7 @@ func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Re sourceName := labels[LabelSourceName] if sourceName == "" { - logger.Info("Internal repository resource missing source-name label, skipping", + logger.Info("Internal repository resource missing cluster-addon-repository label, skipping", "name", obj.GetName(), "namespace", obj.GetNamespace()) return nil diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index 01a65f2..b165eba 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -139,7 +139,10 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * } if err := r.reconcileHelmRepositoryCharts(ctx, repo); err != nil { - return reconcile.Result{RequeueAfter: 30 * time.Second}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling helm repository charts: %w", err), ReasonChartsSyncFailed) + logger.Error(err, "failed to reconcile helm repository charts") + + // TODO: magic number + return reconcile.Result{RequeueAfter: 15 * time.Second}, nil } if op != controllerutil.OperationResultNone { @@ -422,6 +425,8 @@ func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.He // updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (reconcile.Result, error) { + logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) + base := repo.DeepCopy() repo.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) @@ -431,7 +436,12 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1 return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) } - return reconcile.Result{}, nil + // TODO: rework re-index logic based on last sync time attribute + + logger.Info(fmt.Sprintf("Next repository re-scan will be in %s", 5*time.Minute)) + + // TODO: magic number + return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil } // setCondition is a helper to set a single Ready condition on the cluster resource. From dfb2830dba508c15299834dabc0d7b2ee33d2dd1 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 4 Mar 2026 11:17:54 +0300 Subject: [PATCH 15/26] chore: remove stale todos Signed-off-by: Ilya Drey --- api/v1alpha1/helm_cluster_addon_repository.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/api/v1alpha1/helm_cluster_addon_repository.go b/api/v1alpha1/helm_cluster_addon_repository.go index 000f88f..bb94068 100644 --- a/api/v1alpha1/helm_cluster_addon_repository.go +++ b/api/v1alpha1/helm_cluster_addon_repository.go @@ -63,8 +63,6 @@ type HelmClusterAddonRepositorySpec struct { TLSVerify bool `json:"tlsVerify,omitempty"` } -// TODO: define authentication requirements depeding on registry type - type HelmClusterAddonRepositoryAuth struct { // Repository authentication username. // +kubebuilder:validation:MinLength=1 From ecf2fbabd86af24520f0171f654102a230e294eb Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 4 Mar 2026 11:20:58 +0300 Subject: [PATCH 16/26] refactor: re-work repository sync logic Signed-off-by: Ilya Drey --- .../controller/helmclusteraddon/reconciler.go | 18 +-- .../helmclusteraddonchart/reconciler.go | 10 +- .../helmclusteraddonrepository/constants.go | 12 +- .../helmclusteraddonrepository/reconciler.go | 104 ++++++++++-------- .../operator-helm-artifact/pkg/utils/name.go | 22 ++-- templates/admision-policy.yaml | 9 ++ 6 files changed, 98 insertions(+), 77 deletions(-) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index c4fe614..eb85667 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -95,10 +95,7 @@ func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helm existing := &sourcev1.HelmChart{ ObjectMeta: metav1.ObjectMeta{ - Name: utils.GetInternalHelmChartName( - addon.Name, - addon.Spec.Chart.HelmClusterAddonChartName, - addon.Spec.Chart.Version), + Name: utils.GetInternalHelmChartName(addon.Name), Namespace: TargetNamespace, }, } @@ -178,7 +175,7 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 existing := &helmv2.HelmRelease{ ObjectMeta: metav1.ObjectMeta{ - Name: addon.Name, + Name: utils.GetInternalHelmReleaseName(addon.Name), Namespace: TargetNamespace, }, } @@ -204,11 +201,8 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 } existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ - Kind: sourcev1.HelmChartKind, - Name: utils.GetInternalHelmChartName( - addon.Name, - addon.Spec.Chart.HelmClusterAddonChartName, - addon.Spec.Chart.Version), + Kind: sourcev1.HelmChartKind, + Name: utils.GetInternalHelmChartName(addon.Name), Namespace: TargetNamespace, } @@ -250,11 +244,11 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.He return reconcile.Result{}, nil } - if err := r.ensureResourceDeleted(ctx, addon.Name, TargetNamespace, &helmv2.HelmRelease{}); err != nil { + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmReleaseName(addon.Name), TargetNamespace, &helmv2.HelmRelease{}); err != nil { return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm release: %w", err), ReasonCleanupFailed) } - if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(addon.Spec.Chart.HelmClusterAddonRepository, addon.Spec.Chart.HelmClusterAddonChartName, addon.Spec.Chart.Version), TargetNamespace, &sourcev1.HelmChart{}); err != nil { + if err := r.ensureResourceDeleted(ctx, utils.GetInternalHelmChartName(addon.Name), TargetNamespace, &sourcev1.HelmChart{}); err != nil { return reconcile.Result{}, r.patchStatusError(ctx, addon, fmt.Errorf("deleting internal helm chart: %w", err), ReasonCleanupFailed) } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go index 43a1e06..667eb67 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go @@ -37,8 +37,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco logger := log.FromContext(ctx).WithValues("helmclusteraddonchart", req.Name) chart := &helmv1alpha1.HelmClusterAddonChart{} - if err := r.Client.Get(ctx, req.NamespacedName, chart); err != nil { - return ctrl.Result{}, fmt.Errorf("failed to get HelmClusterAddonChart: %w", client.IgnoreNotFound(err)) + if err := r.Client.Get(ctx, req.NamespacedName, chart); client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, fmt.Errorf("failed to get HelmClusterAddonChart: %w", err) } internalCharts := &sourcev1.HelmChartList{} @@ -63,10 +63,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } if needsUpdate { - if err := r.Client.Status().Update(ctx, chart); err != nil { - if client.IgnoreNotFound(err) != nil { - return ctrl.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) - } + if err := r.Client.Status().Update(ctx, chart); client.IgnoreNotFound(err) != nil { + return ctrl.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) } logger.Info("HelmClusterAddonChart successfully reconciled") diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index f04551d..fdc781a 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -31,13 +31,15 @@ const ( // ConditionTypeReady is the condition type for readiness. ConditionTypeReady = "Ready" - // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. - ReasonMirrorSucceeded = "MirrorSucceeded" + // ConditionTypeSynced is the condition type to track chart sync status + ConditionTypeSynced = "Synced" // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. ReasonMirrorFailed = "MirrorFailed" - ReasonChartsSyncFailed = "ChartsSyncFailed" + ReasonSyncSucceeded = "SyncSucceeded" + + ReasonSyncFailed = "SyncFailed" // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. ReasonInternalNotReady = "InternalNotReady" @@ -58,5 +60,7 @@ const ( LabelSourceName = "helm.deckhouse.io/cluster-addon-repository" // DefaultInterval is the default reconciliation interval for the internal repository. - DefaultInterval = 5 * time.Second + DefaultInterval = 5 * time.Minute + + DefaultSyncInterval = 5 * time.Minute ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index b165eba..045f4a9 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -25,6 +25,7 @@ import ( "github.com/deckhouse/operator-helm/pkg/utils" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" @@ -135,29 +136,32 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * return nil }) if err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling helm repository: %w", err), ReasonMirrorFailed) - } - - if err := r.reconcileHelmRepositoryCharts(ctx, repo); err != nil { - logger.Error(err, "failed to reconcile helm repository charts") - - // TODO: magic number - return reconcile.Result{RequeueAfter: 15 * time.Second}, nil + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("reconciling helm repository: %w", err), ReasonMirrorFailed) } if op != controllerutil.OperationResultNone { logger.Info("Successfully reconciled helm repository", "operation", op) + } else { + readyCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeReady) + if readyCond != nil && readyCond.Status == metav1.ConditionTrue { + return r.reconcileHelmRepositoryCharts(ctx, repo) + } } return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) } -func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) error { +func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) + syncCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeSynced) + if syncCond != nil && syncCond.Status == metav1.ConditionTrue && syncCond.LastTransitionTime.Add(DefaultSyncInterval).After(time.Now().UTC()) { + return reconcile.Result{}, nil + } + charts, err := HelmRepositoryDefaultClient.FetchCharts(ctx, repo.Spec.URL) if err != nil { - return fmt.Errorf("cannot fetch chart info from repository: %w", err) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeSynced, fmt.Errorf("cannot fetch chart info from repository: %w", err), ReasonSyncFailed) } for chart, versions := range charts { @@ -184,31 +188,48 @@ func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *he LabelSourceName: repo.Name, } - existingVersionsMap := make(map[string]helmv1alpha1.HelmClusterAddonChartVersion) - for _, version := range existing.Status.Versions { - existingVersionsMap[version.Version] = version - } - - for i, version := range versions { - if existingVersion, found := existingVersionsMap[version.Version]; found && version.Digest == existingVersion.Digest { - versions[i].Pulled = existingVersion.Pulled - } - } - - existing.Status.Versions = versions - return nil }) if err != nil { - return fmt.Errorf("cannot create or update helm chart info: %w", err) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeSynced, fmt.Errorf("cannot create or update helm chart info: %w", err), ReasonSyncFailed) + } + + existingVersionsMap := make(map[string]helmv1alpha1.HelmClusterAddonChartVersion) + for _, version := range existing.Status.Versions { + existingVersionsMap[version.Version] = version + } + + for i, version := range versions { + if existingVersion, found := existingVersionsMap[version.Version]; found && version.Digest == existingVersion.Digest { + versions[i].Pulled = existingVersion.Pulled + } } if op != controllerutil.OperationResultNone { - logger.Info("Successfully reconciled helm repository chart", "operation", op, "repository", repo.Name, "chart", chart) + logger.Info("Successfully reconciled HelmClusterAddonChart", "operation", op, "repository", repo.Name, "chart", chart) } + + base := existing.DeepCopy() + existing.Status.Versions = versions + + if err := r.Client.Status().Patch(ctx, existing, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to update chart status: %w", err) + } + + logger.Info("Successfully sync HelmClusterAddonChart versions", "operation", op, "repository", repo.Name, "chart", chart) } - return nil + base := repo.DeepCopy() + + r.setCondition(repo, ConditionTypeSynced, metav1.ConditionTrue, ReasonSyncSucceeded, "Chart sync succeeded") + + if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + } + + logger.Info(fmt.Sprintf("Scheduling next helm charts sync in %s", DefaultSyncInterval)) + + return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil } func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { @@ -256,11 +277,13 @@ func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *h return nil }) if err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("reconciling oci repository: %w", err), ReasonMirrorFailed) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("reconciling oci repository: %w", err), ReasonMirrorFailed) } if op != controllerutil.OperationResultNone { logger.Info("Successfully reconciled oci repository", "operation", op) + } else { + // TODO: implement chats sync for OCI repository } return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) @@ -372,7 +395,7 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel TargetNamespace, &corev1.Secret{}, ); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal auth secret: %w", err), ReasonCleanupFailed) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal auth secret: %w", err), ReasonCleanupFailed) } if err := r.ensureResourceDeleted( @@ -381,7 +404,7 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel TargetNamespace, &corev1.Secret{}, ); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal tls secret: %w", err), ReasonCleanupFailed) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal tls secret: %w", err), ReasonCleanupFailed) } var internalRepository client.Object @@ -392,11 +415,11 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel case utils.InternalOCIRepository: internalRepository = &sourcev1.OCIRepository{} default: - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("cannot remove unsupported repisotory type: %s", repoType), ReasonCleanupFailed) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("cannot remove unsupported repisotory type: %s", repoType), ReasonCleanupFailed) } if err := r.ensureResourceDeleted(ctx, repo.Name, TargetNamespace, internalRepository); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, fmt.Errorf("deleting internal repository: %w", err), ReasonCleanupFailed) + return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeReady, fmt.Errorf("deleting internal repository: %w", err), ReasonCleanupFailed) } controllerutil.RemoveFinalizer(repo, FinalizerName) @@ -411,10 +434,10 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel } // patchStatusError is a helper to safely patch a failure condition onto the cluster resource. -func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, reconcileErr error, reason string) error { +func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, conditionType string, reconcileErr error, reason string) error { base := repo.DeepCopy() - r.setCondition(repo, metav1.ConditionFalse, reason, reconcileErr.Error()) + r.setCondition(repo, conditionType, metav1.ConditionFalse, reason, reconcileErr.Error()) if patchErr := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); patchErr != nil { return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) @@ -425,8 +448,6 @@ func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.He // updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (reconcile.Result, error) { - logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) - base := repo.DeepCopy() repo.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) @@ -436,20 +457,15 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1 return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) } - // TODO: rework re-index logic based on last sync time attribute - - logger.Info(fmt.Sprintf("Next repository re-scan will be in %s", 5*time.Minute)) - - // TODO: magic number - return reconcile.Result{RequeueAfter: 5 * time.Minute}, nil + return reconcile.Result{}, nil } // setCondition is a helper to set a single Ready condition on the cluster resource. -func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, status metav1.ConditionStatus, reason, message string) { +func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, conditionType string, status metav1.ConditionStatus, reason, message string) { now := metav1.Now() newCond := metav1.Condition{ - Type: ConditionTypeReady, + Type: conditionType, Status: status, Reason: reason, Message: message, @@ -458,7 +474,7 @@ func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, } for i, c := range repo.Status.Conditions { - if c.Type == ConditionTypeReady { + if c.Type == conditionType { // Only update LastTransitionTime if status actually changed. if c.Status == status { newCond.LastTransitionTime = c.LastTransitionTime diff --git a/images/operator-helm-artifact/pkg/utils/name.go b/images/operator-helm-artifact/pkg/utils/name.go index 94f9031..b9cd970 100644 --- a/images/operator-helm-artifact/pkg/utils/name.go +++ b/images/operator-helm-artifact/pkg/utils/name.go @@ -90,23 +90,23 @@ func GetHelmClusterAddonChartName(repoName, addonName string) string { return strings.TrimRight(result, "-") + postfix } -func GetInternalHelmChartName(repoName, chartName, chartVersion string) string { +func GetInternalHelmReleaseName(addonName string) string { prefix := "addon" - hash := GetHash(fmt.Sprintf("%s-%s-%s-%s", prefix, repoName, chartName, chartVersion)) + hash := GetHash(fmt.Sprintf("%s-%s", prefix, addonName)) result := prefix + "-" + postfix := "" - if len(repoName) > 20 { - result += repoName[:20] + if len(addonName) > 40 { + result += addonName[:40] + postfix = "-" + hash } else { - result += repoName + result += addonName } - if len(chartName) > 20 { - result += "-" + chartName[:20] - } else { - result += "-" + chartName - } + return strings.TrimRight(result, "-") + postfix +} - return strings.TrimRight(result, "-") + "-" + hash +func GetInternalHelmChartName(addonName string) string { + return GetInternalHelmReleaseName(addonName) } diff --git a/templates/admision-policy.yaml b/templates/admision-policy.yaml index 5ca4b4d..9339379 100644 --- a/templates/admision-policy.yaml +++ b/templates/admision-policy.yaml @@ -27,6 +27,15 @@ spec: - "UPDATE" - "DELETE" resources: ["*"] + - apiGroups: + - "helm.deckhouse.io" + apiVersions: ["*"] + operations: + - "CREATE" + - "UPDATE" + - "DELETE" + resources: + - "helmclusteraddoncharts" validations: - expression: | request.userInfo.username.startsWith("system:serviceaccount:kube-system:") || From c0bc97ab52ae6752ed324b3f2d8a4968ea9d64a9 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 4 Mar 2026 17:25:59 +0300 Subject: [PATCH 17/26] refactor: re-work HelmClusterAddon statuses Signed-off-by: Ilya Drey --- api/v1alpha1/register.go | 2 +- .../controller/helmclusteraddon/constants.go | 17 +++- .../controller/helmclusteraddon/reconciler.go | 97 ++++++++++++++----- 3 files changed, 88 insertions(+), 28 deletions(-) diff --git a/api/v1alpha1/register.go b/api/v1alpha1/register.go index df4d426..724ccac 100644 --- a/api/v1alpha1/register.go +++ b/api/v1alpha1/register.go @@ -32,7 +32,7 @@ var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} var ( HelmClusterAddonGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} HelmClusterAddonRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonRepostoryKind} - HelmCluserAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} + HelmClusterAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} ) func Kind(kind string) schema.GroupKind { diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go index d9df12c..e7bb307 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -26,8 +26,21 @@ const ( // FinalizerName is the finalizer added to HelmClusterRepository to ensure cleanup. FinalizerName = "helm.deckhouse.io/cleanup" - // ConditionTypeReady is the condition type for readiness. - ConditionTypeReady = "Ready" + ConditionTypeReady = "Ready" + ConditionTypeManaged = "Managed" + ConditionTypeInstalled = "Installed" + ConditionTypeUpdateInstalled = "UpdateInstalled" + ConditionTypeConfigurationApplied = "ConfigurationApplied" + ConditionTypePartiallyDegraded = "PartiallyDegraded" + + ReasonInitializing = "Initializing" + ReasonUnmanagedModeActivated = "UnmanagedModeActivated" + ReasonManagedModeActivated = "ManagedModeActivated" + ReasonInstallationInProgress = "InstallationInProgress" + ReasonDownloading = "Downloading" + ReasonDownloadWasFailed = "DownloadWasFailed" + ReasonUpdateInProgress = "UpdateInProgress" + ReasonUpdateFailed = "UpdateFailed" // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. ReasonMirrorSucceeded = "MirrorSucceeded" diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index eb85667..6e47ceb 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -27,6 +27,7 @@ import ( helmv2 "github.com/werf/3p-helm-controller/api/v2" sourcev1 "github.com/werf/nelm-source-controller/api/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,9 +45,9 @@ type Reconciler struct { func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { logger := log.FromContext(ctx).WithValues("helmclusteraddon", req.Name) - var release helmv1alpha1.HelmClusterAddon + var addon helmv1alpha1.HelmClusterAddon - if err := r.Client.Get(ctx, req.NamespacedName, &release); err != nil { + if err := r.Client.Get(ctx, req.NamespacedName, &addon); err != nil { if apierrors.IsNotFound(err) { logger.Info("HelmClusterAddon not found, skipping") @@ -56,36 +57,48 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddon: %w", err) } - if !release.DeletionTimestamp.IsZero() { - return r.reconcileDelete(ctx, &release) + // Initialize conditions + if meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeReady) == nil { + return r.initializeConditions(ctx, &addon) } - if !controllerutil.ContainsFinalizer(&release, FinalizerName) { - controllerutil.AddFinalizer(&release, FinalizerName) + if !addon.DeletionTimestamp.IsZero() { + return r.reconcileDelete(ctx, &addon) + } + + if !controllerutil.ContainsFinalizer(&addon, FinalizerName) { + controllerutil.AddFinalizer(&addon, FinalizerName) - if err := r.Client.Update(ctx, &release); err != nil { + if err := r.Client.Update(ctx, &addon); err != nil { return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) } return reconcile.Result{}, nil } + // Check if maintenance mode is set + managedCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeManaged) + if managedCond == nil { + return reconcile.Result{}, fmt.Errorf("managed condition is not initialized") + } else if managedCond.Status == metav1.ConditionTrue && addon.Spec.Maintanace != "" { + return reconcile.Result{}, nil + } + var repo helmv1alpha1.HelmClusterAddonRepository - if err := r.Client.Get(ctx, types.NamespacedName{Name: release.Spec.Chart.HelmClusterAddonRepository}, &repo); err != nil { - // TODO: rework this condition + if err := r.Client.Get(ctx, types.NamespacedName{Name: addon.Spec.Chart.HelmClusterAddonRepository}, &repo); err != nil { if apierrors.IsNotFound(err) { - return reconcile.Result{RequeueAfter: 0}, r.patchStatusError(ctx, &release, fmt.Errorf("repository not found: %w", err), ReasonMirrorFailed) + return reconcile.Result{}, r.patchStatusError(ctx, &addon, fmt.Errorf("repository not found: %w", err), ReasonMirrorFailed) } return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddonRepository: %w", err) } - if err := r.reconcileInternalHelmChart(ctx, &release, &repo); err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, &release, fmt.Errorf("internal helm chart reconcile failed: %w", err), ReasonMirrorFailed) + if err := r.reconcileInternalHelmChart(ctx, &addon, &repo); err != nil { + return reconcile.Result{}, r.patchStatusError(ctx, &addon, fmt.Errorf("internal helm chart reconcile failed: %w", err), ReasonMirrorFailed) } - return r.reconcileInternalRelease(ctx, &release) + return r.reconcileInternalRelease(ctx, &addon) } func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { @@ -169,7 +182,7 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 } if !chartPulled { - // TODO: need to reflect the current state in the HelmClusterAddon status. + // TODO: magic number return reconcile.Result{RequeueAfter: 10 * time.Second}, nil } @@ -192,12 +205,10 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 existing.Spec.TargetNamespace = addon.Spec.Namespace existing.Spec.Values = addon.Spec.Values - existing.Spec.DriftDetection = &helmv2.DriftDetection{ - Mode: helmv2.DriftDetectionWarn, - } + existing.Spec.Suspend = false if addon.Spec.Maintanace != "" { - existing.Spec.DriftDetection.Mode = helmv2.DriftDetectionEnabled + existing.Spec.Suspend = true } existing.Spec.ChartRef = &helmv2.CrossNamespaceSourceReference{ @@ -263,11 +274,38 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.He return reconcile.Result{}, nil } +func (r *Reconciler) initializeConditions(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { + conditionTypes := []string{ + ConditionTypeReady, + ConditionTypeManaged, + ConditionTypeConfigurationApplied, + ConditionTypeInstalled, + ConditionTypeUpdateInstalled, + ConditionTypeInstalled, + } + + for _, t := range conditionTypes { + if meta.FindStatusCondition(addon.Status.Conditions, t) == nil { + meta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: t, + Status: metav1.ConditionUnknown, + Reason: ReasonInitializing, + }) + } + } + + if err := r.Client.Status().Update(ctx, addon); err != nil { + return reconcile.Result{}, fmt.Errorf("updating HelmClusterAddon status conditions: %w", err) + } + + return reconcile.Result{}, nil +} + // patchStatusError is a helper to safely patch a failure condition onto the cluster resource. func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { base := addon.DeepCopy() - r.setCondition(addon, metav1.ConditionFalse, reason, reconcileErr.Error()) + r.setCondition(addon, ConditionTypeReady, metav1.ConditionFalse, reason, reconcileErr.Error()) if patchErr := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); patchErr != nil { return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) @@ -280,22 +318,32 @@ func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.H func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalConditions []metav1.Condition) (reconcile.Result, error) { base := addon.DeepCopy() - addon.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) + internalReadyCond := meta.FindStatusCondition(internalConditions, ConditionTypeReady) + if internalReadyCond != nil { + r.setCondition(addon, ConditionTypeReady, internalReadyCond.Status, internalReadyCond.Reason, internalReadyCond.Message) + } + addon.Status.ObservedGeneration = addon.Generation + if addon.Spec.Maintanace == "" { + r.setCondition(addon, ConditionTypeManaged, metav1.ConditionTrue, ReasonManagedModeActivated, "") + } else { + r.setCondition(addon, ConditionTypeManaged, metav1.ConditionFalse, ReasonUnmanagedModeActivated, "") + } + if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { - return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + return reconcile.Result{}, fmt.Errorf("updating HelmClusterAddon status on success: %w", err) } return reconcile.Result{}, nil } // setCondition is a helper to set a single Ready condition on the cluster resource. -func (r *Reconciler) setCondition(addon *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string) { +func (r *Reconciler) setCondition(addon *helmv1alpha1.HelmClusterAddon, conditionType string, status metav1.ConditionStatus, reason, message string) { now := metav1.Now() newCond := metav1.Condition{ - Type: ConditionTypeReady, + Type: conditionType, Status: status, Reason: reason, Message: message, @@ -304,8 +352,7 @@ func (r *Reconciler) setCondition(addon *helmv1alpha1.HelmClusterAddon, status m } for i, c := range addon.Status.Conditions { - if c.Type == ConditionTypeReady { - // Only update LastTransitionTime if status actually changed. + if c.Type == conditionType { if c.Status == status { newCond.LastTransitionTime = c.LastTransitionTime } From 9ac3aed0a44d953dad746ebb1cfdebbadde3fdb2 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Wed, 4 Mar 2026 22:14:30 +0300 Subject: [PATCH 18/26] fix: correct repository sync schedule Signed-off-by: Ilya Drey --- .../helmclusteraddonrepository/constants.go | 6 +- .../helmclusteraddonrepository/reconciler.go | 103 ++++++++++++++---- 2 files changed, 83 insertions(+), 26 deletions(-) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index fdc781a..ad5a4c7 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -37,9 +37,9 @@ const ( // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. ReasonMirrorFailed = "MirrorFailed" - ReasonSyncSucceeded = "SyncSucceeded" - - ReasonSyncFailed = "SyncFailed" + ReasonSyncSucceeded = "SyncSucceeded" + ReasonSyncInProgress = "ReasonSyncInProgress" + ReasonSyncFailed = "SyncFailed" // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. ReasonInternalNotReady = "InternalNotReady" diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index 045f4a9..3fb4c4e 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -77,7 +77,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, fmt.Errorf("adding finalizer: %w", err) } - return reconcile.Result{}, nil + return r.requeueAtSyncInterval(&repo) } switch repoType { @@ -86,7 +86,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco case utils.InternalOCIRepository: return r.reconcileInternalOCIRepository(ctx, &repo) default: - return reconcile.Result{}, nil + return r.requeueAtSyncInterval(&repo) } } @@ -141,22 +141,33 @@ func (r *Reconciler) reconcileInternalHelmRepository(ctx context.Context, repo * if op != controllerutil.OperationResultNone { logger.Info("Successfully reconciled helm repository", "operation", op) - } else { - readyCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeReady) - if readyCond != nil && readyCond.Status == metav1.ConditionTrue { - return r.reconcileHelmRepositoryCharts(ctx, repo) - } } - return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) + if changed, err := r.updateSuccessStatus(ctx, repo, existing.Status.Conditions); err != nil { + return reconcile.Result{}, fmt.Errorf("updating status after repository reconcile: %w", err) + } else if changed { + return r.requeueAtSyncInterval(repo) + } + + if apimeta.IsStatusConditionPresentAndEqual(repo.Status.Conditions, ConditionTypeReady, metav1.ConditionTrue) { + return r.reconcileHelmRepositoryCharts(ctx, repo) + } + + return r.requeueAtSyncInterval(repo) } func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { logger := log.FromContext(ctx) syncCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeSynced) - if syncCond != nil && syncCond.Status == metav1.ConditionTrue && syncCond.LastTransitionTime.Add(DefaultSyncInterval).After(time.Now().UTC()) { - return reconcile.Result{}, nil + if syncCond != nil && syncCond.Status == metav1.ConditionTrue && syncCond.LastTransitionTime.UTC().Add(DefaultSyncInterval).After(time.Now().UTC()) { + return r.requeueAtSyncInterval(repo) + } else if syncCond == nil || syncCond.Reason != ReasonSyncInProgress { + if err := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncInProgress, ""); err != nil { + return reconcile.Result{}, fmt.Errorf("updating sync condition: %w", err) + } + + return r.requeueAtSyncInterval(repo) } charts, err := HelmRepositoryDefaultClient.FetchCharts(ctx, repo.Spec.URL) @@ -191,7 +202,11 @@ func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *he return nil }) if err != nil { - return reconcile.Result{}, r.patchStatusError(ctx, repo, ConditionTypeSynced, fmt.Errorf("cannot create or update helm chart info: %w", err), ReasonSyncFailed) + if statusUpdateErr := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncFailed, ""); statusUpdateErr != nil { + return reconcile.Result{}, fmt.Errorf("failed to update sync condition: %w", err) + } + + return reconcile.Result{}, fmt.Errorf("cannot create or update HelmClusterAddonChart: %w", err) } existingVersionsMap := make(map[string]helmv1alpha1.HelmClusterAddonChartVersion) @@ -213,23 +228,41 @@ func (r *Reconciler) reconcileHelmRepositoryCharts(ctx context.Context, repo *he existing.Status.Versions = versions if err := r.Client.Status().Patch(ctx, existing, client.MergeFrom(base)); err != nil { + if statusUpdateErr := r.updateSyncCondition(ctx, repo, metav1.ConditionFalse, ReasonSyncFailed, ""); statusUpdateErr != nil { + return reconcile.Result{}, fmt.Errorf("failed to update sync condition: %w", err) + } + return reconcile.Result{}, fmt.Errorf("failed to update chart status: %w", err) } logger.Info("Successfully sync HelmClusterAddonChart versions", "operation", op, "repository", repo.Name, "chart", chart) } + logger.Info(fmt.Sprintf("Scheduling next helm charts sync in %s", DefaultSyncInterval)) + + if err := r.updateSyncCondition(ctx, repo, metav1.ConditionTrue, ReasonSyncSucceeded, ""); err != nil { + return reconcile.Result{}, fmt.Errorf("updating sync condition: %w", err) + } + + return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil +} + +func (r *Reconciler) updateSyncCondition(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, status metav1.ConditionStatus, reason, message string) error { base := repo.DeepCopy() - r.setCondition(repo, ConditionTypeSynced, metav1.ConditionTrue, ReasonSyncSucceeded, "Chart sync succeeded") + apimeta.SetStatusCondition(&repo.Status.Conditions, metav1.Condition{ + Type: ConditionTypeSynced, + Status: status, + ObservedGeneration: repo.Generation, + Reason: reason, + Message: message, + }) if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { - return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + return err } - logger.Info(fmt.Sprintf("Scheduling next helm charts sync in %s", DefaultSyncInterval)) - - return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil + return nil } func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { @@ -286,7 +319,11 @@ func (r *Reconciler) reconcileInternalOCIRepository(ctx context.Context, repo *h // TODO: implement chats sync for OCI repository } - return r.updateSuccessStatus(ctx, repo, existing.Status.Conditions) + if _, err := r.updateSuccessStatus(ctx, repo, existing.Status.Conditions); err != nil { + return reconcile.Result{}, err + } + + return r.requeueAtSyncInterval(repo) } func (r *Reconciler) reconcileInternalRepositoryAuthSecret(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) error { @@ -446,18 +483,38 @@ func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.He return reconcileErr } +func (r *Reconciler) requeueAtSyncInterval(repo *helmv1alpha1.HelmClusterAddonRepository) (reconcile.Result, error) { + repoSyncCond := apimeta.FindStatusCondition(repo.Status.Conditions, ConditionTypeSynced) + if repoSyncCond != nil { + remaining := time.Until(repoSyncCond.LastTransitionTime.Add(DefaultSyncInterval)) + if remaining > 0 { + return reconcile.Result{RequeueAfter: remaining}, nil + } + } + + return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil +} + // updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. -func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (reconcile.Result, error) { +func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (bool, error) { + var changed bool + base := repo.DeepCopy() - repo.Status.Conditions = MapInternalStatusToClusterConditions(internalConditions) - repo.Status.ObservedGeneration = repo.Generation + internalReadyCond := apimeta.FindStatusCondition(internalConditions, meta.ReadyCondition) + if internalReadyCond != nil { + changed = apimeta.SetStatusCondition(&repo.Status.Conditions, *internalReadyCond) + } - if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { - return reconcile.Result{}, fmt.Errorf("patching internal custom resource status: %w", err) + if changed { + repo.Status.ObservedGeneration = repo.Generation + + if err := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); err != nil { + return false, fmt.Errorf("patching status: %w", err) + } } - return reconcile.Result{}, nil + return changed, nil } // setCondition is a helper to set a single Ready condition on the cluster resource. From ef5c3387d80db956f89d7b20c468ab7fadf724d2 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 00:04:56 +0300 Subject: [PATCH 19/26] fix: correct typos Signed-off-by: Ilya Drey --- api/v1alpha1/helm_cluster_addon.go | 8 ++++---- api/v1alpha1/helm_cluster_addon_repository.go | 4 ++-- api/v1alpha1/register.go | 2 +- crds/helmclusteraddonrepositories.yaml | 4 ++-- crds/helmclusteraddons.yaml | 2 +- .../pkg/api/openapi/zz_generated.openapi.go | 4 ++-- .../pkg/controller/helmclusteraddon/reconciler.go | 6 +++--- 7 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index ac689f6..02d611d 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -77,7 +77,7 @@ type HelmClusterAddonSpec struct { // When empty (""), standard reconciliation is active. // +kubebuilder:validation:Enum="";NoResourceReconciliation // +optional - Maintanace string `json:"maintanace,omitempty"` + Maintenance string `json:"maintenance,omitempty"` } type HelmClusterAddonChartRef struct { @@ -115,12 +115,12 @@ type HelmClusterAddonList struct { Items []HelmClusterAddon `json:"items"` } -// HelmClusterAddonMaintanace describe HelmClusterAddon maintanance operation mode. +// HelmClusterAddonMaintenance describe HelmClusterAddon maintanance operation mode. // +kubebuilder:validation:Enum={"",NoResourceReconciliation} -type HelmClusterAddonMaintanace string +type HelmClusterAddonMaintenance string const ( - NoResourceReconciliation HelmClusterAddonMaintanace = "NoResourceReconciliation" + NoResourceReconciliation HelmClusterAddonMaintenance = "NoResourceReconciliation" ) // +k8s:deepcopy-gen=false diff --git a/api/v1alpha1/helm_cluster_addon_repository.go b/api/v1alpha1/helm_cluster_addon_repository.go index bb94068..00d71ed 100644 --- a/api/v1alpha1/helm_cluster_addon_repository.go +++ b/api/v1alpha1/helm_cluster_addon_repository.go @@ -21,11 +21,11 @@ import ( ) const ( - HelmClusterAddonRepostoryKind = "HelmClusterAddonRepository" + HelmClusterAddonRepositoryKind = "HelmClusterAddonRepository" HelmClusterAddonRepositoryResource = "helmclusteraddonrepositories" ) -// HelmClusterAddonRepository represens a Git, Helm or OCI complient repocitory with Helm charts. +// HelmClusterAddonRepository represents a Git, Helm or OCI compliant repository with Helm charts. // // +kubebuilder:object:root=true // +kubebuilder:subresource:status diff --git a/api/v1alpha1/register.go b/api/v1alpha1/register.go index 724ccac..daacae9 100644 --- a/api/v1alpha1/register.go +++ b/api/v1alpha1/register.go @@ -31,7 +31,7 @@ var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} var ( HelmClusterAddonGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} - HelmClusterAddonRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonRepostoryKind} + HelmClusterAddonRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonRepositoryKind} HelmClusterAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} ) diff --git a/crds/helmclusteraddonrepositories.yaml b/crds/helmclusteraddonrepositories.yaml index da1680a..de1b5ba 100644 --- a/crds/helmclusteraddonrepositories.yaml +++ b/crds/helmclusteraddonrepositories.yaml @@ -29,8 +29,8 @@ spec: schema: openAPIV3Schema: description: - HelmClusterAddonRepository represens a Git, Helm or OCI complient - repocitory with Helm charts. + HelmClusterAddonRepository represents a Git, Helm or OCI compliant + repository with Helm charts. properties: apiVersion: description: |- diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml index 7fcec2e..37998a9 100644 --- a/crds/helmclusteraddons.yaml +++ b/crds/helmclusteraddons.yaml @@ -82,7 +82,7 @@ spec: - helmClusterAddonChart - helmClusterAddonRepository type: object - maintanace: + maintenance: description: |- Maintenance specifies the reconciliation strategy for the resource. When set to "NoResourceReconciliation", the controller will stop updating the diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index 8bb8180..26d4b4f 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -436,7 +436,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref return common.OpenAPIDefinition{ Schema: spec.Schema{ SchemaProps: spec.SchemaProps{ - Description: "HelmClusterAddonRepository represens a Git, Helm or OCI complient repocitory with Helm charts.", + Description: "HelmClusterAddonRepository represents a Git, Helm or OCI compliant repository with Helm charts.", Type: []string{"object"}, Properties: map[string]spec.Schema{ "kind": { @@ -663,7 +663,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref common Format: "", }, }, - "maintanace": { + "maintenance": { SchemaProps: spec.SchemaProps{ Description: "Maintenance specifies the reconciliation strategy for the resource. When set to \"NoResourceReconciliation\", the controller will stop updating the underlying resources, allowing for manual intervention or maintenance without the operator overwriting changes. When empty (\"\"), standard reconciliation is active.", Type: []string{"string"}, diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index 6e47ceb..aa34ea9 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -80,7 +80,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco managedCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeManaged) if managedCond == nil { return reconcile.Result{}, fmt.Errorf("managed condition is not initialized") - } else if managedCond.Status == metav1.ConditionTrue && addon.Spec.Maintanace != "" { + } else if managedCond.Status == metav1.ConditionTrue && addon.Spec.Maintenance != "" { return reconcile.Result{}, nil } @@ -207,7 +207,7 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 existing.Spec.Suspend = false - if addon.Spec.Maintanace != "" { + if addon.Spec.Maintenance != "" { existing.Spec.Suspend = true } @@ -325,7 +325,7 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha addon.Status.ObservedGeneration = addon.Generation - if addon.Spec.Maintanace == "" { + if addon.Spec.Maintenance == "" { r.setCondition(addon, ConditionTypeManaged, metav1.ConditionTrue, ReasonManagedModeActivated, "") } else { r.setCondition(addon, ConditionTypeManaged, metav1.ConditionFalse, ReasonUnmanagedModeActivated, "") From d92312c389e43e5fe77b98d09be4417d29ab393b Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 00:13:33 +0300 Subject: [PATCH 20/26] refactor: update addon maintenance mode Signed-off-by: Ilya Drey --- .../pkg/controller/helmclusteraddon/reconciler.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index aa34ea9..01c4918 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -80,7 +80,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco managedCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeManaged) if managedCond == nil { return reconcile.Result{}, fmt.Errorf("managed condition is not initialized") - } else if managedCond.Status == metav1.ConditionTrue && addon.Spec.Maintenance != "" { + } else if managedCond.Status == metav1.ConditionFalse && addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { return reconcile.Result{}, nil } @@ -207,7 +207,7 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 existing.Spec.Suspend = false - if addon.Spec.Maintenance != "" { + if addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { existing.Spec.Suspend = true } @@ -325,10 +325,10 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha addon.Status.ObservedGeneration = addon.Generation - if addon.Spec.Maintenance == "" { - r.setCondition(addon, ConditionTypeManaged, metav1.ConditionTrue, ReasonManagedModeActivated, "") - } else { + if addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { r.setCondition(addon, ConditionTypeManaged, metav1.ConditionFalse, ReasonUnmanagedModeActivated, "") + } else { + r.setCondition(addon, ConditionTypeManaged, metav1.ConditionTrue, ReasonManagedModeActivated, "") } if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { From 7fdef772e6cf1e2a08ba19e79771c1b2cb6cc117 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 11:44:05 +0300 Subject: [PATCH 21/26] refactor: remove unused files Signed-off-by: Ilya Drey --- .../helmclusteraddonrepository/mapper.go | 64 ------------------- 1 file changed, 64 deletions(-) delete mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go deleted file mode 100644 index cc9b384..0000000 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/mapper.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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 helmclusteraddonrepository - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// TODO: need to re-work these statuses according to adr - -func MapInternalStatusToClusterConditions(internalConditions []metav1.Condition) []metav1.Condition { - now := metav1.Now() - - var readyCond *metav1.Condition - - for i := range internalConditions { - if internalConditions[i].Type == ConditionTypeReady { - readyCond = &internalConditions[i] - - break - } - } - - if readyCond == nil { - return []metav1.Condition{ - { - Type: ConditionTypeReady, - Status: metav1.ConditionUnknown, - Reason: ReasonInternalNotReady, - Message: "Processing", - LastTransitionTime: now, - }, - } - } - - reason := ReasonInternalNotReady - if readyCond.Status == metav1.ConditionTrue { - reason = ReasonInternalReady - } - - return []metav1.Condition{ - { - Type: ConditionTypeReady, - Status: readyCond.Status, - Reason: reason, - Message: readyCond.Message, - LastTransitionTime: now, - }, - } -} From 2fe1db5bcd773dc1f4076fa8b43710902f1f0b11 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 11:45:50 +0300 Subject: [PATCH 22/26] refactor: improve HelmClusterAddon status observability Signed-off-by: Ilya Drey --- .../controller/helmclusteraddon/constants.go | 3 + .../pkg/controller/helmclusteraddon/mapper.go | 64 -------- .../controller/helmclusteraddon/reconciler.go | 153 ++++++++++++++---- 3 files changed, 121 insertions(+), 99 deletions(-) delete mode 100644 images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go index e7bb307..ea68d3a 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -36,10 +36,13 @@ const ( ReasonInitializing = "Initializing" ReasonUnmanagedModeActivated = "UnmanagedModeActivated" ReasonManagedModeActivated = "ManagedModeActivated" + ReasonUpdateSucceeded = "UpdateSucceeded" + ReasonInstallSucceeded = "InstallSucceeded" ReasonInstallationInProgress = "InstallationInProgress" ReasonDownloading = "Downloading" ReasonDownloadWasFailed = "DownloadWasFailed" ReasonUpdateInProgress = "UpdateInProgress" + ReasonInstallFailed = "InstallFailed" ReasonUpdateFailed = "UpdateFailed" // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go deleted file mode 100644 index 1095617..0000000 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/mapper.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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 helmclusteraddon - -import ( - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -// TODO: need to re-work these statuses according to adr - -func MapInternalStatusToClusterConditions(internalConditions []metav1.Condition) []metav1.Condition { - now := metav1.Now() - - var readyCond *metav1.Condition - - for i := range internalConditions { - if internalConditions[i].Type == ConditionTypeReady { - readyCond = &internalConditions[i] - - break - } - } - - if readyCond == nil { - return []metav1.Condition{ - { - Type: ConditionTypeReady, - Status: metav1.ConditionUnknown, - Reason: ReasonInternalNotReady, - Message: "Processing", - LastTransitionTime: now, - }, - } - } - - reason := ReasonInternalNotReady - if readyCond.Status == metav1.ConditionTrue { - reason = ReasonInternalReady - } - - return []metav1.Condition{ - { - Type: ConditionTypeReady, - Status: readyCond.Status, - Reason: reason, - Message: readyCond.Message, - LastTransitionTime: now, - }, - } -} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index 01c4918..44caf49 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -28,6 +28,7 @@ import ( sourcev1 "github.com/werf/nelm-source-controller/api/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -98,7 +99,7 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, r.patchStatusError(ctx, &addon, fmt.Errorf("internal helm chart reconcile failed: %w", err), ReasonMirrorFailed) } - return r.reconcileInternalRelease(ctx, &addon) + return r.reconcileInternalHelmRelease(ctx, &addon) } func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, repo *helmv1alpha1.HelmClusterAddonRepository) error { @@ -154,7 +155,7 @@ func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helm return nil } -func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { +func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { logger := log.FromContext(ctx) var addonChart helmv1alpha1.HelmClusterAddonChart @@ -227,7 +228,7 @@ func (r *Reconciler) reconcileInternalRelease(ctx context.Context, addon *helmv1 logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", addon.Spec.Chart.HelmClusterAddonChartName) } - return r.updateSuccessStatus(ctx, addon, existing.Status.Conditions) + return r.updateStatus(ctx, addon, existing) } // ensureResourceDeleted safely deletes an object if it exists. @@ -278,10 +279,6 @@ func (r *Reconciler) initializeConditions(ctx context.Context, addon *helmv1alph conditionTypes := []string{ ConditionTypeReady, ConditionTypeManaged, - ConditionTypeConfigurationApplied, - ConditionTypeInstalled, - ConditionTypeUpdateInstalled, - ConditionTypeInstalled, } for _, t := range conditionTypes { @@ -301,11 +298,32 @@ func (r *Reconciler) initializeConditions(ctx context.Context, addon *helmv1alph return reconcile.Result{}, nil } -// patchStatusError is a helper to safely patch a failure condition onto the cluster resource. func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, reconcileErr error, reason string) error { base := addon.DeepCopy() - r.setCondition(addon, ConditionTypeReady, metav1.ConditionFalse, reason, reconcileErr.Error()) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: metav1.ConditionFalse, + Reason: reason, + Message: reconcileErr.Error(), + }) + + updateInstalledCond := meta.FindStatusCondition(base.Status.Conditions, ConditionTypeUpdateInstalled) + if updateInstalledCond != nil { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: reconcileErr.Error(), + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: reconcileErr.Error(), + }) + } if patchErr := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); patchErr != nil { return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) @@ -314,21 +332,96 @@ func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.H return reconcileErr } -// updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. -func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalConditions []metav1.Condition) (reconcile.Result, error) { +func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) (reconcile.Result, error) { base := addon.DeepCopy() - internalReadyCond := meta.FindStatusCondition(internalConditions, ConditionTypeReady) + internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) if internalReadyCond != nil { - r.setCondition(addon, ConditionTypeReady, internalReadyCond.Status, internalReadyCond.Reason, internalReadyCond.Message) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: internalReadyCond.Status, + Reason: internalReadyCond.Reason, + Message: internalReadyCond.Message, + }) + + switch internalReadyCond.Reason { + case helmv2.InstallSucceededReason: + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionTrue, + Reason: ReasonInstallSucceeded, + Message: "", + }) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonInstallSucceeded, + Message: "", + }) + case helmv2.UpgradeSucceededReason: + if r.isUpdateInstalled(internalHelmRelease) { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + Message: "", + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + Message: "", + }) + } + case helmv2.InstallFailedReason: + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: internalReadyCond.Message, + }) + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionFalse, + Reason: ReasonInstallFailed, + Message: internalReadyCond.Message, + }) + case helmv2.UpgradeFailedReason: + if r.isUpdateInstalled(internalHelmRelease) { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: internalReadyCond.Message, + }) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionFalse, + Reason: ReasonUpdateFailed, + Message: internalReadyCond.Message, + }) + } + } } addon.Status.ObservedGeneration = addon.Generation if addon.Spec.Maintenance == string(helmv1alpha1.NoResourceReconciliation) { - r.setCondition(addon, ConditionTypeManaged, metav1.ConditionFalse, ReasonUnmanagedModeActivated, "") + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeManaged, + Status: metav1.ConditionFalse, + Reason: ReasonUnmanagedModeActivated, + Message: "", + }) } else { - r.setCondition(addon, ConditionTypeManaged, metav1.ConditionTrue, ReasonManagedModeActivated, "") + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeManaged, + Status: metav1.ConditionTrue, + Reason: ReasonManagedModeActivated, + Message: "", + }) } if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { @@ -338,30 +431,20 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, addon *helmv1alpha return reconcile.Result{}, nil } -// setCondition is a helper to set a single Ready condition on the cluster resource. -func (r *Reconciler) setCondition(addon *helmv1alpha1.HelmClusterAddon, conditionType string, status metav1.ConditionStatus, reason, message string) { - now := metav1.Now() - - newCond := metav1.Condition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - LastTransitionTime: now, - ObservedGeneration: addon.Generation, +func (r *Reconciler) isUpdateInstalled(internalHelmRelease *helmv2.HelmRelease) bool { + internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) + if internalReadyCond == nil { + return false } - for i, c := range addon.Status.Conditions { - if c.Type == conditionType { - if c.Status == status { - newCond.LastTransitionTime = c.LastTransitionTime - } - - addon.Status.Conditions[i] = newCond + if internalReadyCond.Status == metav1.ConditionTrue && internalHelmRelease.Status.History.Len() > 1 { + latest := internalHelmRelease.Status.History.Latest() + previous := internalHelmRelease.Status.History.Previous(true) - return + if previous != nil && previous.Status == "superseded" && latest != nil && latest.VersionedChartName() != previous.VersionedChartName() { + return true } } - addon.Status.Conditions = append(addon.Status.Conditions, newCond) + return false } From 9fa19b469f4815285515c9442b0715e1a2f3de0c Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 13:09:25 +0300 Subject: [PATCH 23/26] fix: correct HelmClusterAddonChart reconcile Signed-off-by: Ilya Drey --- .../pkg/controller/helmclusteraddonchart/reconciler.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go index 667eb67..1202ab1 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/reconciler.go @@ -41,6 +41,12 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return ctrl.Result{}, fmt.Errorf("failed to get HelmClusterAddonChart: %w", err) } + if !chart.DeletionTimestamp.IsZero() { + return reconcile.Result{}, nil + } + + base := chart.DeepCopy() + internalCharts := &sourcev1.HelmChartList{} if err := r.Client.List(ctx, internalCharts, client.InNamespace(TargetNamespace)); err != nil { return ctrl.Result{}, fmt.Errorf("failed to list internal helm chart list: %w", err) @@ -63,8 +69,8 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco } if needsUpdate { - if err := r.Client.Status().Update(ctx, chart); client.IgnoreNotFound(err) != nil { - return ctrl.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) + if err := r.Client.Status().Patch(ctx, chart, client.MergeFrom(base)); err != nil { + return reconcile.Result{}, fmt.Errorf("failed to update HelmClusterAddonChart status: %w", err) } logger.Info("HelmClusterAddonChart successfully reconciled") From ebaee00aca5c4a89709325be9a597f1e8a41231f Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Thu, 5 Mar 2026 18:24:49 +0300 Subject: [PATCH 24/26] feat: add LastAppliedValues and LastAppliedChart support Signed-off-by: Ilya Drey --- api/go.mod | 8 +- api/go.sum | 18 +- api/v1alpha1/helm_cluster_addon.go | 21 ++ api/v1alpha1/register.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 22 +++ crds/helmclusteraddons.yaml | 25 +++ images/hooks/go.mod | 80 ++++---- images/hooks/go.sum | 166 +++++----------- images/operator-helm-artifact/go.mod | 17 +- images/operator-helm-artifact/go.sum | 49 +++-- .../pkg/api/openapi/zz_generated.openapi.go | 181 +++++++++++------- .../controller/helmclusteraddon/reconciler.go | 33 +++- 12 files changed, 350 insertions(+), 272 deletions(-) diff --git a/api/go.mod b/api/go.mod index 4eaa1f4..c4ab374 100644 --- a/api/go.mod +++ b/api/go.mod @@ -26,9 +26,9 @@ require ( github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.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/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect @@ -38,7 +38,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/api/go.sum b/api/go.sum index 883263f..e4b8369 100644 --- a/api/go.sum +++ b/api/go.sum @@ -3,7 +3,6 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 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= @@ -20,14 +19,9 @@ github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj2 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-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-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 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/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= @@ -49,16 +43,12 @@ github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFF 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/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -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/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index 02d611d..ecd9488 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -97,6 +97,12 @@ type HelmClusterAddonChartRef struct { } type HelmClusterAddonStatus struct { + // LastAppliedChart represents the latest chart that triggered addon install or update. + // +optional + LastAppliedChart HelmClusterAddonLastAppliedChartRef `json:"lastAppliedChart,omitempty"` + // LastAppliedValues represents the latest values that triggered addon install or update. + // +optional + LastAppliedValues *apiextensionsv1.JSON `json:"lastAppliedValues,omitempty"` // Conditions represent the latest available observations of the repository state. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` @@ -104,6 +110,21 @@ type HelmClusterAddonStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } +type HelmClusterAddonLastAppliedChartRef struct { + // Specifies the name of the Helm chart to be installed + // from the defined repository (e.g., "ingress-nginx" or "redis"). + // +optional + HelmClusterAddonChartName string `json:"helmClusterAddonChart,omitempty"` + // Specifies the name of the HelmClusterAddonRepository custom resource that contains + // the connection details and credentials for the repository where + // the chart is located. + // +optional + HelmClusterAddonRepository string `json:"helmClusterAddonRepository,omitempty"` + // Versions holds the HelmClusterAddon chart version. + // +optional + Version string `json:"version,omitempty"` +} + // HelmClusterAddonList contains a list of HelmClusterAddons. // +kubebuilder:object:root=true // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/api/v1alpha1/register.go b/api/v1alpha1/register.go index daacae9..1e7fd46 100644 --- a/api/v1alpha1/register.go +++ b/api/v1alpha1/register.go @@ -32,7 +32,7 @@ var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: Version} var ( HelmClusterAddonGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} HelmClusterAddonRepositoryGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonRepositoryKind} - HelmClusterAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonKind} + HelmClusterAddonChartGVK = schema.GroupVersionKind{Group: SchemeGroupVersion.Group, Version: SchemeGroupVersion.Version, Kind: HelmClusterAddonChartKind} ) func Kind(kind string) schema.GroupKind { diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e71c9f4..55c4abf 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -191,6 +191,22 @@ func (in *HelmClusterAddonChartVersion) DeepCopy() *HelmClusterAddonChartVersion return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmClusterAddonLastAppliedChartRef) DeepCopyInto(out *HelmClusterAddonLastAppliedChartRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmClusterAddonLastAppliedChartRef. +func (in *HelmClusterAddonLastAppliedChartRef) DeepCopy() *HelmClusterAddonLastAppliedChartRef { + if in == nil { + return nil + } + out := new(HelmClusterAddonLastAppliedChartRef) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmClusterAddonList) DeepCopyInto(out *HelmClusterAddonList) { *out = *in @@ -370,6 +386,12 @@ func (in *HelmClusterAddonSpec) DeepCopy() *HelmClusterAddonSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmClusterAddonStatus) DeepCopyInto(out *HelmClusterAddonStatus) { *out = *in + out.LastAppliedChart = in.LastAppliedChart + if in.LastAppliedValues != nil { + in, out := &in.LastAppliedValues, &out.LastAppliedValues + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } if in.Conditions != nil { in, out := &in.Conditions, &out.Conditions *out = make([]v1.Condition, len(*in)) diff --git a/crds/helmclusteraddons.yaml b/crds/helmclusteraddons.yaml index 37998a9..46bfe43 100644 --- a/crds/helmclusteraddons.yaml +++ b/crds/helmclusteraddons.yaml @@ -167,6 +167,31 @@ spec: - type type: object type: array + lastAppliedChart: + description: + LastAppliedChart represents the latest chart that triggered + addon install or update. + properties: + helmClusterAddonChart: + description: |- + Specifies the name of the Helm chart to be installed + from the defined repository (e.g., "ingress-nginx" or "redis"). + type: string + helmClusterAddonRepository: + description: |- + Specifies the name of the HelmClusterAddonRepository custom resource that contains + the connection details and credentials for the repository where + the chart is located. + type: string + version: + description: Versions holds the HelmClusterAddon chart version. + type: string + type: object + lastAppliedValues: + description: + LastAppliedValues represents the latest values that triggered + addon install or update. + x-kubernetes-preserve-unknown-fields: true observedGeneration: description: Generating a resource that was last processed by the diff --git a/images/hooks/go.mod b/images/hooks/go.mod index c2c7496..6404416 100644 --- a/images/hooks/go.mod +++ b/images/hooks/go.mod @@ -16,78 +16,82 @@ require ( 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/emicklei/go-restful/v3 v3.12.2 // indirect github.com/ettle/strcase v0.2.0 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // 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/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.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/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/gojuno/minimock/v3 v3.4.7 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/certificate-transparency-go v1.1.7 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/go-containerregistry v0.20.6 // indirect + github.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/jonboulle/clockwork v0.4.0 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/jonboulle/clockwork v0.5.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/mailru/easyjson v0.9.0 // 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/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/onsi/gomega v1.38.3 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pelletier/go-toml v1.9.3 // 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.22.0 // indirect - github.com/prometheus/client_model v0.6.1 // indirect - github.com/prometheus/common v0.62.0 // indirect - github.com/prometheus/procfs v0.15.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.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/spf13/cobra v1.10.2 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/sylabs/oci-tools v0.7.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/tidwall/pretty v1.2.1 // indirect github.com/vbatts/tar-split v0.12.1 // indirect github.com/weppos/publicsuffix-go v0.30.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/zmap/zcrypto v0.0.0-20230310154051-c8b263fd8300 // indirect github.com/zmap/zlint/v3 v3.5.0 // indirect - golang.org/x/crypto v0.36.0 // indirect - golang.org/x/net v0.38.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.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.30.0 // indirect - golang.org/x/text v0.23.0 // indirect - golang.org/x/time v0.9.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect - google.golang.org/protobuf v1.36.5 // indirect - gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.33.8 // indirect - k8s.io/apiextensions-apiserver v0.33.8 // indirect - k8s.io/apimachinery v0.33.8 // indirect - k8s.io/client-go v0.33.8 // indirect + k8s.io/api v0.35.1 // indirect + k8s.io/apiextensions-apiserver v0.35.1 // indirect + k8s.io/apimachinery v0.35.1 // indirect + k8s.io/client-go v0.35.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // 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 + k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 // indirect + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect + sigs.k8s.io/controller-runtime v0.23.1 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/images/hooks/go.sum b/images/hooks/go.sum index e9f18d4..dbc33f6 100644 --- a/images/hooks/go.sum +++ b/images/hooks/go.sum @@ -1,5 +1,6 @@ github.com/DataDog/gostackparse v0.7.0 h1:i7dLkXHvYzHV308hnkvVGDL3BR4FWl7IsXNPz/IGQh4= github.com/DataDog/gostackparse v0.7.0/go.mod h1:lTfqcJKqS9KnXQGnyQMCugq3u1FP6UZMfWR0aitKFMM= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 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= @@ -11,7 +12,6 @@ github.com/cloudflare/cfssl v1.6.5/go.mod h1:Bk1si7sq8h2+yVEDrFJiz3d7Aw+pfjjJSZV 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= @@ -26,44 +26,30 @@ github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBi 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/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 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.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= 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/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 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-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= 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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/certificate-transparency-go v1.1.7 h1:IASD+NtgSTJLPdzkthwvAG1ZVbF2WtFg4IvoA68XGSw= github.com/google/certificate-transparency-go v1.1.7/go.mod h1:FSSBo8fyMVgqptbfF6j5p/XNdgQftAhSmXcIxV9iphE= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 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= @@ -73,8 +59,7 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO 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/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ= 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= @@ -83,22 +68,17 @@ github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4= -github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= 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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 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= @@ -107,25 +87,20 @@ 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/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= 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/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d/go.mod h1:OQA4XLvDbMgS8P0CevmM4m9Q3Jq4phKUzcocxuGJ5m8= 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/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= @@ -138,16 +113,11 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE 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.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= -github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= -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.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= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= @@ -158,25 +128,17 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 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/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/sylabs/oci-tools v0.7.0 h1:SIisUvcEL+Vpa9/kmQDy1W3AwV2XVGad83sgZmXLlb0= github.com/sylabs/oci-tools v0.7.0/go.mod h1:Ry6ngChflh20WPq6mLvCKSw2OTd9iDB5aR8OQzeq4hM= github.com/sylabs/sif/v2 v2.15.0 h1:Nv0tzksFnoQiQ2eUwpAis9nVqEu4c3RcNSxX8P3Cecw= @@ -185,8 +147,8 @@ 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/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= 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/weppos/publicsuffix-go v0.12.0/go.mod h1:z3LCPQ38eedDQSwmsSRW4Y7t2L8Ln16JPQ02lHAdn5k= @@ -196,8 +158,6 @@ github.com/weppos/publicsuffix-go v0.30.0/go.mod h1:kBi8zwYnR0zrbm8RcuN1o9Fzgpnn github.com/weppos/publicsuffix-go/publicsuffix/generator v0.0.0-20220927085643-dc0d00c92642/go.mod h1:GHfoeIdZLdZmLjMlzBftbTDntahTttUMWjxZwQJhULE= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zmap/rc2 v0.0.0-20131011165748-24b9757f5521/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= github.com/zmap/rc2 v0.0.0-20190804163417-abaa70531248/go.mod h1:3YZ9o3WnatTIZhuOtot4IcUfzoKVjUHqu6WALIyI0nE= @@ -216,25 +176,22 @@ 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= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= -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/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= 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-20200202094626-16171245cfb2/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.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -242,17 +199,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -266,47 +219,35 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/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/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= 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.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= -golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= -golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= 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.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= -google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 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-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/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= 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= @@ -316,28 +257,17 @@ 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.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -k8s.io/api v0.33.8 h1:IPju/eyOsfnIN4EVR9U5qrxe1CpNBZi+JtvvfKLvq6s= -k8s.io/api v0.33.8/go.mod h1:POmJWNXzip1LpvMhSWpjWmbzgyoa1Rt0FRxMwe2s7QA= -k8s.io/apiextensions-apiserver v0.33.8 h1:7zHBQBsZYyZu2Tay++lIUdjexxCxcr5TinL11y/d3HA= -k8s.io/apiextensions-apiserver v0.33.8/go.mod h1:phLEQv7OSxpMbDr7dZYSd+WNzKCQ8TdNJU5iCHyo1OM= -k8s.io/apimachinery v0.33.8 h1:aiUOauowBiUz4IFJktqxAMSHx4AYmmqB1Yq4QgnaZ6E= -k8s.io/apimachinery v0.33.8/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.8 h1:RX5exRQynxnGB5LJdviBaZXSR3vn3LWp3/bn87+wl8w= -k8s.io/client-go v0.33.8/go.mod h1:MA2z0/7JdSqTx9BAfvl+Vng+maMGRcn3ZkURrx5rKUs= +k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= +k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= +k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= +k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= 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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -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/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 h1:HhDfevmPS+OalTjQRKbTHppRIz01AWi8s45TMXStgYY= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/images/operator-helm-artifact/go.mod b/images/operator-helm-artifact/go.mod index c4843c5..0eb2c1b 100644 --- a/images/operator-helm-artifact/go.mod +++ b/images/operator-helm-artifact/go.mod @@ -6,10 +6,13 @@ replace github.com/deckhouse/operator-helm/api => ../../api require ( github.com/deckhouse/operator-helm/api v0.0.0-00010101000000-000000000000 + github.com/opencontainers/go-digest v1.0.0 github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 + github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 github.com/werf/3p-helm-controller/api v0.1.4 github.com/werf/nelm-source-controller/api v0.1.4 go.yaml.in/yaml/v3 v3.0.4 + helm.sh/helm/v3 v3.19.2 k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 @@ -19,8 +22,10 @@ require ( ) require ( + github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cyphar/filepath-securejoin v0.6.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect @@ -28,24 +33,28 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/zapr v1.3.0 // 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/go-openapi/jsonpointer v0.21.1 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.1 // indirect github.com/google/btree v1.1.3 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.7.7 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // 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 + 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.23.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.66.1 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 // indirect github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 // indirect diff --git a/images/operator-helm-artifact/go.sum b/images/operator-helm-artifact/go.sum index e631ff7..e94cb26 100644 --- a/images/operator-helm-artifact/go.sum +++ b/images/operator-helm-artifact/go.sum @@ -1,14 +1,19 @@ +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= +github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= 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/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= @@ -23,14 +28,12 @@ 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-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= +github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= +github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= 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/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= @@ -52,17 +55,18 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 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= @@ -75,6 +79,10 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +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/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= @@ -88,24 +96,24 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/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/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1 h1:b1P4avYWjjWuzPSOv6QZtk1ffl/iBfWBGK4qNAxaA94= github.com/werf/3p-fluxcd-pkg/apis/acl v0.9.0-nelm.1/go.mod h1:00dBUg4SN+4Xu4LWrbQm5LdmRKVP9Fjbvb+rvqjHrVI= github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1 h1:edZ5ugpeUvmjG+g9laet8qTBqDdQPl18aNr6k0xqdYY= +github.com/werf/3p-fluxcd-pkg/apis/kustomize v1.14.0-nelm.1/go.mod h1:dAboSMVeohict/XrpXrqyZodq+8Qp6dwafzkBzoCHcU= github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1 h1:rYX8cMeryBHH7sNPVSQm1IAVES08TiWvADaZsDj98Wk= github.com/werf/3p-fluxcd-pkg/apis/meta v1.23.0-nelm.1/go.mod h1:14co1+Ub5rW0Bp3Qo4IzCHwEcaw06StyMu7Rv5pMVCY= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1 h1:ua0xt66rxKptzbG1zxy3u96qfV8XsFT9Jd2PU8L6mc8= +github.com/werf/3p-fluxcd-pkg/chartutil v1.17.0-nelm.1/go.mod h1:fodaCyMGXGxYYSIdWvokrjki8e+DAhgu6BtzHbH2VJ8= github.com/werf/3p-helm-controller/api v0.1.4 h1:s7g9UQOrDMUzVE+JtWOP2xApnPOKYlNe1tXkkWCisAw= github.com/werf/3p-helm-controller/api v0.1.4/go.mod h1:tiPvDerlc5SwKIDmXB8L3kIMJHse+wigueoEGQq+588= github.com/werf/nelm-source-controller/api v0.1.4 h1:/k3RT+hHdwKHntoebdcjhO+zboJIlljHJZlbcumoY08= @@ -151,9 +159,10 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.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.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= +helm.sh/helm/v3 v3.19.2 h1:psQjaM8aIWrSVEly6PgYtLu/y6MRSmok4ERiGhZmtUY= +helm.sh/helm/v3 v3.19.2/go.mod h1:gX10tB5ErM+8fr7bglUUS/UfTOO8UUTYWIBH1IYNnpE= k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= k8s.io/apiextensions-apiserver v0.35.1 h1:p5vvALkknlOcAqARwjS20kJffgzHqwyQRM8vHLwgU7w= diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index 26d4b4f..808f3b1 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -30,72 +30,73 @@ import ( func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition { return map[string]common.OpenAPIDefinition{ - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), - "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonValidator": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref), - v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), - v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), - v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), - v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), - v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), - v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), - v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), - v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), - v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), - v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), - v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), - v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), - v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), - v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), - v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), - v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), - v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), - v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), - v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), - v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), - v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), - v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), - v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), - v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), - v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), - v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), - v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), - v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), - v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), - v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), - v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), - v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), - v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), - v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), - v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), - v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), - v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), - v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), - v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), - v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), - v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), - v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), - v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), - v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), - v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), - v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), - v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), - v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), - v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), - version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddon": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddon(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChart": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChart(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartRef(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonChartVersion": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonLastAppliedChartRef(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepository": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepository(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryAuth": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryAuth(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryList": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryList(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositorySpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositorySpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonRepositoryStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonRepositoryStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonSpec": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonSpec(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonStatus": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref), + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonValidator": schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonValidator(ref), + v1.APIGroup{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroup(ref), + v1.APIGroupList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIGroupList(ref), + v1.APIResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResource(ref), + v1.APIResourceList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIResourceList(ref), + v1.APIVersions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_APIVersions(ref), + v1.ApplyOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ApplyOptions(ref), + v1.Condition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Condition(ref), + v1.CreateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_CreateOptions(ref), + v1.DeleteOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_DeleteOptions(ref), + v1.Duration{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Duration(ref), + v1.FieldSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldSelectorRequirement(ref), + v1.FieldsV1{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_FieldsV1(ref), + v1.GetOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GetOptions(ref), + v1.GroupKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupKind(ref), + v1.GroupResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupResource(ref), + v1.GroupVersion{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersion(ref), + v1.GroupVersionForDiscovery{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionForDiscovery(ref), + v1.GroupVersionKind{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionKind(ref), + v1.GroupVersionResource{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_GroupVersionResource(ref), + v1.InternalEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_InternalEvent(ref), + v1.LabelSelector{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelector(ref), + v1.LabelSelectorRequirement{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_LabelSelectorRequirement(ref), + v1.List{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_List(ref), + v1.ListMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListMeta(ref), + v1.ListOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ListOptions(ref), + v1.ManagedFieldsEntry{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ManagedFieldsEntry(ref), + v1.MicroTime{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_MicroTime(ref), + v1.ObjectMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ObjectMeta(ref), + v1.OwnerReference{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_OwnerReference(ref), + v1.PartialObjectMetadata{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadata(ref), + v1.PartialObjectMetadataList{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PartialObjectMetadataList(ref), + v1.Patch{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Patch(ref), + v1.PatchOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_PatchOptions(ref), + v1.Preconditions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Preconditions(ref), + v1.RootPaths{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_RootPaths(ref), + v1.ServerAddressByClientCIDR{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_ServerAddressByClientCIDR(ref), + v1.Status{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Status(ref), + v1.StatusCause{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusCause(ref), + v1.StatusDetails{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_StatusDetails(ref), + v1.Table{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Table(ref), + v1.TableColumnDefinition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableColumnDefinition(ref), + v1.TableOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableOptions(ref), + v1.TableRow{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRow(ref), + v1.TableRowCondition{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TableRowCondition(ref), + v1.Time{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Time(ref), + v1.Timestamp{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_Timestamp(ref), + v1.TypeMeta{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_TypeMeta(ref), + v1.UpdateOptions{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_UpdateOptions(ref), + v1.WatchEvent{}.OpenAPIModelName(): schema_pkg_apis_meta_v1_WatchEvent(ref), + version.Info{}.OpenAPIModelName(): schema_k8sio_apimachinery_pkg_version_Info(ref), } } @@ -382,6 +383,39 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonChartVersion(re } } +func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonLastAppliedChartRef(ref common.ReferenceCallback) common.OpenAPIDefinition { + return common.OpenAPIDefinition{ + Schema: spec.Schema{ + SchemaProps: spec.SchemaProps{ + Type: []string{"object"}, + Properties: map[string]spec.Schema{ + "helmClusterAddonChart": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the Helm chart to be installed from the defined repository (e.g., \"ingress-nginx\" or \"redis\").", + Type: []string{"string"}, + Format: "", + }, + }, + "helmClusterAddonRepository": { + SchemaProps: spec.SchemaProps{ + Description: "Specifies the name of the HelmClusterAddonRepository custom resource that contains the connection details and credentials for the repository where the chart is located.", + Type: []string{"string"}, + Format: "", + }, + }, + "version": { + SchemaProps: spec.SchemaProps{ + Description: "Versions holds the HelmClusterAddon chart version.", + Type: []string{"string"}, + Format: "", + }, + }, + }, + }, + }, + } +} + func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonList(ref common.ReferenceCallback) common.OpenAPIDefinition { return common.OpenAPIDefinition{ Schema: spec.Schema{ @@ -685,6 +719,19 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref comm SchemaProps: spec.SchemaProps{ Type: []string{"object"}, Properties: map[string]spec.Schema{ + "lastAppliedChart": { + SchemaProps: spec.SchemaProps{ + Description: "LastAppliedChart represents the latest chart that triggered addon install or update.", + Default: map[string]interface{}{}, + Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef"), + }, + }, + "lastAppliedValues": { + SchemaProps: spec.SchemaProps{ + Description: "LastAppliedValues represents the latest values that triggered addon install or update.", + Ref: ref("k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON"), + }, + }, "conditions": { SchemaProps: spec.SchemaProps{ Description: "Conditions represent the latest available observations of the repository state.", @@ -710,7 +757,7 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref comm }, }, Dependencies: []string{ - v1.Condition{}.OpenAPIModelName()}, + "github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef", "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1.JSON", v1.Condition{}.OpenAPIModelName()}, } } diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index 44caf49..a356d82 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -24,8 +24,11 @@ import ( "github.com/deckhouse/operator-helm/pkg/controller/helmclusteraddonchart" "github.com/deckhouse/operator-helm/pkg/utils" + "github.com/opencontainers/go-digest" + "github.com/werf/3p-fluxcd-pkg/chartutil" helmv2 "github.com/werf/3p-helm-controller/api/v2" sourcev1 "github.com/werf/nelm-source-controller/api/v1" + helmchartutil "helm.sh/helm/v3/pkg/chartutil" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" apimeta "k8s.io/apimachinery/pkg/api/meta" @@ -333,6 +336,8 @@ func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.H } func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) (reconcile.Result, error) { + logger := log.FromContext(ctx) + base := addon.DeepCopy() internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) @@ -358,6 +363,12 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC Reason: ReasonInstallSucceeded, Message: "", }) + addon.Status.LastAppliedChart = helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, + Version: base.Spec.Chart.Version, + } + addon.Status.LastAppliedValues = base.Spec.Values case helmv2.UpgradeSucceededReason: if r.isUpdateInstalled(internalHelmRelease) { apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ @@ -367,12 +378,22 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC Message: "", }) } else { - apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ - Type: ConditionTypeConfigurationApplied, - Status: metav1.ConditionTrue, - Reason: ReasonUpdateSucceeded, - Message: "", - }) + if addonValues, err := helmchartutil.ReadValues(addon.Spec.Values.Raw); err != nil { + logger.Error(err, "failed to decode values on LastAppliedValues update: %w", err) + } else { + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + Message: "Applied configuration with values digest " + internalHelmRelease.Status.History.Latest().ConfigDigest, + }) + + latestRelease := internalHelmRelease.Status.History.Latest() + + if latestRelease != nil && latestRelease.Status == "deployed" && latestRelease.ConfigDigest == chartutil.DigestValues(digest.Canonical, addonValues).String() { + addon.Status.LastAppliedValues = addon.Spec.Values + } + } } case helmv2.InstallFailedReason: apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ From c8fe65b815a13926023dfde03d41855abb5f69ca Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Fri, 6 Mar 2026 10:35:37 +0300 Subject: [PATCH 25/26] refactor: unify reconcile methods across controllers Signed-off-by: Ilya Drey --- api/v1alpha1/helm_cluster_addon.go | 2 +- api/v1alpha1/zz_generated.deepcopy.go | 6 +- .../pkg/api/openapi/zz_generated.openapi.go | 1 - .../controller/helmclusteraddon/constants.go | 27 +++++---- .../controller/helmclusteraddon/controller.go | 48 ++------------- .../controller/helmclusteraddon/reconciler.go | 28 ++++----- .../helmclusteraddonchart/constants.go | 9 ++- .../helmclusteraddonchart/controller.go | 44 ++------------ .../helmclusteraddonrepository/constants.go | 18 +++--- .../helmclusteraddonrepository/controller.go | 48 ++------------- .../helmclusteraddonrepository/reconciler.go | 40 ++----------- .../pkg/utils/mapper.go | 59 +++++++++++++++++++ 12 files changed, 132 insertions(+), 198 deletions(-) create mode 100644 images/operator-helm-artifact/pkg/utils/mapper.go diff --git a/api/v1alpha1/helm_cluster_addon.go b/api/v1alpha1/helm_cluster_addon.go index ecd9488..bf63e85 100644 --- a/api/v1alpha1/helm_cluster_addon.go +++ b/api/v1alpha1/helm_cluster_addon.go @@ -99,7 +99,7 @@ type HelmClusterAddonChartRef struct { type HelmClusterAddonStatus struct { // LastAppliedChart represents the latest chart that triggered addon install or update. // +optional - LastAppliedChart HelmClusterAddonLastAppliedChartRef `json:"lastAppliedChart,omitempty"` + LastAppliedChart *HelmClusterAddonLastAppliedChartRef `json:"lastAppliedChart,omitempty"` // LastAppliedValues represents the latest values that triggered addon install or update. // +optional LastAppliedValues *apiextensionsv1.JSON `json:"lastAppliedValues,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 55c4abf..0348940 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -386,7 +386,11 @@ func (in *HelmClusterAddonSpec) DeepCopy() *HelmClusterAddonSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HelmClusterAddonStatus) DeepCopyInto(out *HelmClusterAddonStatus) { *out = *in - out.LastAppliedChart = in.LastAppliedChart + if in.LastAppliedChart != nil { + in, out := &in.LastAppliedChart, &out.LastAppliedChart + *out = new(HelmClusterAddonLastAppliedChartRef) + **out = **in + } if in.LastAppliedValues != nil { in, out := &in.LastAppliedValues, &out.LastAppliedValues *out = new(apiextensionsv1.JSON) diff --git a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go index 808f3b1..4acd2b1 100644 --- a/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go +++ b/images/operator-helm-artifact/pkg/api/openapi/zz_generated.openapi.go @@ -722,7 +722,6 @@ func schema_deckhouse_operator_helm_api_v1alpha1_HelmClusterAddonStatus(ref comm "lastAppliedChart": { SchemaProps: spec.SchemaProps{ Description: "LastAppliedChart represents the latest chart that triggered addon install or update.", - Default: map[string]interface{}{}, Ref: ref("github.com/deckhouse/operator-helm/api/v1alpha1.HelmClusterAddonLastAppliedChartRef"), }, }, diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go index ea68d3a..0c9ea8e 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/constants.go @@ -16,6 +16,8 @@ limitations under the License. package helmclusteraddon +import "time" + const ( // ControllerName is the name of this controller, used for leader election and logging. ControllerName = "helmclusteraddon-controller" @@ -45,24 +47,27 @@ const ( ReasonInstallFailed = "InstallFailed" ReasonUpdateFailed = "UpdateFailed" - // ReasonMirrorSucceeded indicates the internal HelmRepository was created/updated successfully. - ReasonMirrorSucceeded = "MirrorSucceeded" - // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. ReasonMirrorFailed = "MirrorFailed" - // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. - ReasonInternalNotReady = "InternalNotReady" - - // ReasonInternalReady indicates the internal HelmRepository has reported Ready. - ReasonInternalReady = "InternalReady" - // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. ReasonCleanupFailed = "CleanupFailed" + // ReasonProcessing indicates that facade resource is processing. ReasonProcessing = "Processing" - LabelManagedBy = "helm.deckhouse.io/managed-by" + // LabelManagedBy marks resources as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. LabelManagedByValue = "operator-helm" - LabelSourceName = "helm.deckhouse.io/cluster-addon" + + // LabelSourceName stores the name of the source facade resource. + LabelSourceName = "helm.deckhouse.io/cluster-addon" + + // InternalReleaseDeployed indicates that specific release fon internal chart release history was deployed + InternalReleaseDeployed = "deployed" + + // ChartPullInterval how ofter to check if specific version of facade chart was pulled. + ChartPullInterval = 2 * time.Second ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go index eedfbee..53946c4 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/controller.go @@ -17,20 +17,14 @@ limitations under the License. package helmclusteraddon import ( - "context" - - "k8s.io/apimachinery/pkg/types" + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + helmv2 "github.com/werf/3p-helm-controller/api/v2" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - helmv2 "github.com/werf/3p-helm-controller/api/v2" - sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) func SetupWithManager(mgr ctrl.Manager) error { @@ -43,43 +37,13 @@ func SetupWithManager(mgr ctrl.Manager) error { For(&helmv1alpha1.HelmClusterAddon{}). Watches( &sourcev1.HelmChart{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Watches( &helmv2.HelmRelease{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Complete(r) } - -func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { - logger := log.FromContext(ctx) - - if obj.GetNamespace() != TargetNamespace { - return nil - } - - labels := obj.GetLabels() - if labels[LabelManagedBy] != LabelManagedByValue { - return nil - } - - sourceName := labels[LabelSourceName] - if sourceName == "" { - logger.Info("Internal repository resource missing cluster-addon label, skipping", - "name", obj.GetName(), "namespace", obj.GetNamespace()) - - return nil - } - - return []reconcile.Request{ - { - NamespacedName: types.NamespacedName{ - Name: sourceName, - Namespace: "", - }, - }, - } -} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index a356d82..739d339 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -61,7 +61,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, fmt.Errorf("getting HelmClusterAddon: %w", err) } - // Initialize conditions if meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeReady) == nil { return r.initializeConditions(ctx, &addon) } @@ -80,7 +79,6 @@ func (r *Reconciler) Reconcile(ctx context.Context, req reconcile.Request) (reco return reconcile.Result{}, nil } - // Check if maintenance mode is set managedCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeManaged) if managedCond == nil { return reconcile.Result{}, fmt.Errorf("managed condition is not initialized") @@ -186,8 +184,7 @@ func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *he } if !chartPulled { - // TODO: magic number - return reconcile.Result{RequeueAfter: 10 * time.Second}, nil + return reconcile.Result{RequeueAfter: ChartPullInterval}, nil } existing := &helmv2.HelmRelease{ @@ -234,7 +231,6 @@ func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *he return r.updateStatus(ctx, addon, existing) } -// ensureResourceDeleted safely deletes an object if it exists. func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { if apierrors.IsNotFound(err) { @@ -251,7 +247,6 @@ func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace return nil } -// reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. func (r *Reconciler) reconcileDelete(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon) (reconcile.Result, error) { logger := log.FromContext(ctx).WithValues("helmclusteraddon", addon.Name) @@ -363,7 +358,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC Reason: ReasonInstallSucceeded, Message: "", }) - addon.Status.LastAppliedChart = helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, Version: base.Spec.Chart.Version, @@ -381,17 +376,19 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC if addonValues, err := helmchartutil.ReadValues(addon.Spec.Values.Raw); err != nil { logger.Error(err, "failed to decode values on LastAppliedValues update: %w", err) } else { - apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ - Type: ConditionTypeConfigurationApplied, - Status: metav1.ConditionTrue, - Reason: ReasonUpdateSucceeded, - Message: "Applied configuration with values digest " + internalHelmRelease.Status.History.Latest().ConfigDigest, - }) - latestRelease := internalHelmRelease.Status.History.Latest() - if latestRelease != nil && latestRelease.Status == "deployed" && latestRelease.ConfigDigest == chartutil.DigestValues(digest.Canonical, addonValues).String() { + if latestRelease != nil && latestRelease.Status == InternalReleaseDeployed && + latestRelease.ConfigDigest == chartutil.DigestValues(digest.Canonical, addonValues).String() { addon.Status.LastAppliedValues = addon.Spec.Values + + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeConfigurationApplied, + Status: metav1.ConditionTrue, + Reason: ReasonUpdateSucceeded, + LastTransitionTime: metav1.NewTime(time.Now()), + Message: "Applied configuration with values digest " + internalHelmRelease.Status.History.Latest().ConfigDigest, + }) } } } @@ -452,6 +449,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC return reconcile.Result{}, nil } +// isUpdateInstalled return true if new release was initiated due to chart name/version change, otherwise returns false. func (r *Reconciler) isUpdateInstalled(internalHelmRelease *helmv2.HelmRelease) bool { internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) if internalReadyCond == nil { diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go index fba4b3e..7e8edd2 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/constants.go @@ -23,7 +23,12 @@ const ( // TargetNamespace is the namespace where internal customer resources are created. TargetNamespace = "d8-operator-helm" - LabelManagedBy = "helm.deckhouse.io/managed-by" + // LabelManagedBy marks resources as managed by this controller. + LabelManagedBy = "helm.deckhouse.io/managed-by" + + // LabelManagedByValue is the value for the managed-by label. LabelManagedByValue = "operator-helm" - LabelSourceName = "helm.deckhouse.io/cluster-addon-chart" + + // LabelSourceName stores the name of the source facade resource. + LabelSourceName = "helm.deckhouse.io/cluster-addon-chart" ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go index 1a2a5a9..db5d0d3 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonchart/controller.go @@ -17,19 +17,13 @@ limitations under the License. package helmclusteraddonchart import ( - "context" - - "k8s.io/apimachinery/pkg/types" + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) func SetupWithManager(mgr ctrl.Manager) error { @@ -42,38 +36,8 @@ func SetupWithManager(mgr ctrl.Manager) error { For(&helmv1alpha1.HelmClusterAddonChart{}). Watches( &sourcev1.HelmChart{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Complete(r) } - -func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { - logger := log.FromContext(ctx) - - if obj.GetNamespace() != TargetNamespace { - return nil - } - - labels := obj.GetLabels() - if labels[LabelManagedBy] != LabelManagedByValue { - return nil - } - - sourceName := labels[LabelSourceName] - if sourceName == "" { - logger.Info("Internal repository resource missing cluster-addon-chart label, skipping", - "name", obj.GetName(), "namespace", obj.GetNamespace()) - - return nil - } - - return []reconcile.Request{ - { - NamespacedName: types.NamespacedName{ - Name: sourceName, - Namespace: "", - }, - }, - } -} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go index ad5a4c7..7459ebe 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/constants.go @@ -37,30 +37,30 @@ const ( // ReasonMirrorFailed indicates the internal HelmRepository create/update failed. ReasonMirrorFailed = "MirrorFailed" - ReasonSyncSucceeded = "SyncSucceeded" - ReasonSyncInProgress = "ReasonSyncInProgress" - ReasonSyncFailed = "SyncFailed" + // ReasonSyncSucceeded indicates that chart sync was successfully completed. + ReasonSyncSucceeded = "SyncSucceeded" - // ReasonInternalNotReady indicates the internal HelmRepository is not yet ready. - ReasonInternalNotReady = "InternalNotReady" + // ReasonSyncInProgress indicates that chart sync is in progress. + ReasonSyncInProgress = "ReasonSyncInProgress" - // ReasonInternalReady indicates the internal HelmRepository has reported Ready. - ReasonInternalReady = "InternalReady" + // ReasonSyncFailed indicates that charts sync was failed. + ReasonSyncFailed = "SyncFailed" // ReasonCleanupFailed indicates deletion of the internal HelmRepository failed. ReasonCleanupFailed = "CleanupFailed" - // LabelManagedBy marks an internal HelmRepository as managed by this controller. + // LabelManagedBy marks resources as managed by this controller. LabelManagedBy = "helm.deckhouse.io/managed-by" // LabelManagedByValue is the value for the managed-by label. LabelManagedByValue = "operator-helm" - // LabelSourceName stores the name of the source HelmClusterAddonRepository. + // LabelSourceName stores the name of the source facade resource. LabelSourceName = "helm.deckhouse.io/cluster-addon-repository" // DefaultInterval is the default reconciliation interval for the internal repository. DefaultInterval = 5 * time.Minute + // DefaultSyncInterval is the default repository charts sync interval. DefaultSyncInterval = 5 * time.Minute ) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go index 5e51729..6509b09 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/controller.go @@ -17,20 +17,14 @@ limitations under the License. package helmclusteraddonrepository import ( - "context" - + helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" + "github.com/deckhouse/operator-helm/pkg/utils" + sourcev1 "github.com/werf/nelm-source-controller/api/v1" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/predicate" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - helmv1alpha1 "github.com/deckhouse/operator-helm/api/v1alpha1" - sourcev1 "github.com/werf/nelm-source-controller/api/v1" ) func SetupWithManager(mgr ctrl.Manager) error { @@ -43,48 +37,18 @@ func SetupWithManager(mgr ctrl.Manager) error { For(&helmv1alpha1.HelmClusterAddonRepository{}). Watches( &sourcev1.HelmRepository{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Watches( &sourcev1.OCIRepository{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Watches( &corev1.Secret{}, - handler.EnqueueRequestsFromMapFunc(mapInternalToCluster), + handler.EnqueueRequestsFromMapFunc(utils.MapInternalToFacade(TargetNamespace, LabelManagedBy, LabelManagedByValue, LabelSourceName)), builder.WithPredicates(predicate.ResourceVersionChangedPredicate{}), ). Complete(r) } - -func mapInternalToCluster(ctx context.Context, obj client.Object) []reconcile.Request { - logger := log.FromContext(ctx) - - if obj.GetNamespace() != TargetNamespace { - return nil - } - - labels := obj.GetLabels() - if labels[LabelManagedBy] != LabelManagedByValue { - return nil - } - - sourceName := labels[LabelSourceName] - if sourceName == "" { - logger.Info("Internal repository resource missing cluster-addon-repository label, skipping", - "name", obj.GetName(), "namespace", obj.GetNamespace()) - - return nil - } - - return []reconcile.Request{ - { - NamespacedName: types.NamespacedName{ - Name: sourceName, - Namespace: "", - }, - }, - } -} diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go index 3fb4c4e..9f39862 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddonrepository/reconciler.go @@ -401,7 +401,6 @@ func (r *Reconciler) reconcileInternalRepositoryTLSSecret(ctx context.Context, r return nil } -// ensureResourceDeleted safely deletes an object if it exists. func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { if err := r.Client.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, obj); err != nil { if apierrors.IsNotFound(err) { @@ -418,7 +417,6 @@ func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace return nil } -// reconcileDelete handles cleanup when the HelmClusterRepository is being deleted. func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, repoType utils.InternalRepositoryType) (reconcile.Result, error) { logger := log.FromContext(ctx).WithValues("helmclusteraddonrepository", repo.Name) @@ -470,11 +468,15 @@ func (r *Reconciler) reconcileDelete(ctx context.Context, repo *helmv1alpha1.Hel return reconcile.Result{}, nil } -// patchStatusError is a helper to safely patch a failure condition onto the cluster resource. func (r *Reconciler) patchStatusError(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, conditionType string, reconcileErr error, reason string) error { base := repo.DeepCopy() - r.setCondition(repo, conditionType, metav1.ConditionFalse, reason, reconcileErr.Error()) + apimeta.SetStatusCondition(&repo.Status.Conditions, metav1.Condition{ + Type: conditionType, + Status: metav1.ConditionFalse, + Reason: reason, + Message: reconcileErr.Error(), + }) if patchErr := r.Client.Status().Patch(ctx, repo, client.MergeFrom(base)); patchErr != nil { return errors.Join(reconcileErr, fmt.Errorf("failed to patch status: %w", patchErr)) @@ -495,7 +497,6 @@ func (r *Reconciler) requeueAtSyncInterval(repo *helmv1alpha1.HelmClusterAddonRe return reconcile.Result{RequeueAfter: DefaultSyncInterval}, nil } -// updateSuccessStatus patches the status of the cluster resource after a successful reconciliation. func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1.HelmClusterAddonRepository, internalConditions []metav1.Condition) (bool, error) { var changed bool @@ -516,32 +517,3 @@ func (r *Reconciler) updateSuccessStatus(ctx context.Context, repo *helmv1alpha1 return changed, nil } - -// setCondition is a helper to set a single Ready condition on the cluster resource. -func (r *Reconciler) setCondition(repo *helmv1alpha1.HelmClusterAddonRepository, conditionType string, status metav1.ConditionStatus, reason, message string) { - now := metav1.Now() - - newCond := metav1.Condition{ - Type: conditionType, - Status: status, - Reason: reason, - Message: message, - LastTransitionTime: now, - ObservedGeneration: repo.Generation, - } - - for i, c := range repo.Status.Conditions { - if c.Type == conditionType { - // Only update LastTransitionTime if status actually changed. - if c.Status == status { - newCond.LastTransitionTime = c.LastTransitionTime - } - - repo.Status.Conditions[i] = newCond - - return - } - } - - repo.Status.Conditions = append(repo.Status.Conditions, newCond) -} diff --git a/images/operator-helm-artifact/pkg/utils/mapper.go b/images/operator-helm-artifact/pkg/utils/mapper.go new file mode 100644 index 0000000..c3f390b --- /dev/null +++ b/images/operator-helm-artifact/pkg/utils/mapper.go @@ -0,0 +1,59 @@ +/* +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 utils + +import ( + "context" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +func MapInternalToFacade(targetNamespace, labelManagedBy, labelManagedByValue, labelSourceName string) handler.MapFunc { + return func(ctx context.Context, obj client.Object) []reconcile.Request { + logger := log.FromContext(ctx) + + if obj.GetNamespace() != targetNamespace { + return nil + } + + labels := obj.GetLabels() + if labels[labelManagedBy] != labelManagedByValue { + return nil + } + + sourceName := labels[labelSourceName] + if sourceName == "" { + logger.Info("resource missing source label, skipping", + "name", obj.GetName(), "namespace", obj.GetNamespace()) + + return nil + } + + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Name: sourceName, + Namespace: "", + }, + }, + } + } +} From bbd15f6866359f133fa1b8112cc44b5199f06e61 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Fri, 6 Mar 2026 11:43:34 +0300 Subject: [PATCH 26/26] fix: update LastAppliedChart on upgrade Signed-off-by: Ilya Drey --- .../controller/helmclusteraddon/reconciler.go | 103 ++++++++++++++++-- 1 file changed, 91 insertions(+), 12 deletions(-) diff --git a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go index 739d339..2966c94 100644 --- a/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go +++ b/images/operator-helm-artifact/pkg/controller/helmclusteraddon/reconciler.go @@ -153,6 +153,67 @@ func (r *Reconciler) reconcileInternalHelmChart(ctx context.Context, addon *helm logger.Info("Successfully reconciled internal helm chart", "operation", op, "repository", repo.Name, "chart", addon.Spec.Chart.HelmClusterAddonChartName) } + reconcileCond := meta.FindStatusCondition(existing.Status.Conditions, "Reconciling") + if reconcileCond != nil { + if err := r.updateStatusOnInternalHelmChart(ctx, addon, metav1.ConditionFalse, reconcileCond.Reason, reconcileCond.Message, true); err != nil { + return fmt.Errorf("cannot update HelmClusterAddon status: %w", err) + } + + return reconcile.TerminalError(fmt.Errorf("internal helm chart %s is processing", existing.Name)) + } + + readyCond := meta.FindStatusCondition(existing.Status.Conditions, ConditionTypeReady) + if readyCond != nil && readyCond.Status == metav1.ConditionFalse { + if err := r.updateStatusOnInternalHelmChart(ctx, addon, metav1.ConditionFalse, readyCond.Reason, readyCond.Message, false); err != nil { + return fmt.Errorf("cannot update HelmClusterAddon status: %w", err) + } + + return reconcile.TerminalError(fmt.Errorf("internal helm chart %s is not ready", existing.Name)) + } + + return nil +} + +func (r *Reconciler) updateStatusOnInternalHelmChart(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, status metav1.ConditionStatus, reason, message string, inProgress bool) error { + base := addon.DeepCopy() + + installedCond := meta.FindStatusCondition(base.Status.Conditions, ConditionTypeInstalled) + updateInstalledCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeUpdateInstalled) + if updateInstalledCond != nil || (installedCond != nil && installedCond.Status == metav1.ConditionTrue) { + if inProgress { + reason = ReasonUpdateInProgress + message = "" + } + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeUpdateInstalled, + Status: status, + Reason: reason, + Message: message, + }) + } else { + if inProgress { + reason = ReasonInstallationInProgress + message = "" + } + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeInstalled, + Status: status, + Reason: reason, + Message: message, + }) + } + + apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: ConditionTypeReady, + Status: status, + Reason: reason, + Message: message, + }) + + if err := r.Client.Status().Patch(ctx, addon, client.MergeFrom(base)); err != nil { + return fmt.Errorf("updating HelmClusterAddon status on success: %w", err) + } + return nil } @@ -165,7 +226,7 @@ func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *he ctx, types.NamespacedName{ Name: utils.GetHelmClusterAddonChartName(addon.Spec.Chart.HelmClusterAddonRepository, - addon.Spec.Chart.HelmClusterAddonRepository), + addon.Spec.Chart.HelmClusterAddonChartName), }, &addonChart, ); err != nil { @@ -228,7 +289,7 @@ func (r *Reconciler) reconcileInternalHelmRelease(ctx context.Context, addon *he logger.Info("Successfully reconciled internal helm release", "operation", op, "chart", addon.Spec.Chart.HelmClusterAddonChartName) } - return r.updateStatus(ctx, addon, existing) + return r.updateStatusOnInternalRelease(ctx, addon, existing) } func (r *Reconciler) ensureResourceDeleted(ctx context.Context, name, namespace string, obj client.Object) error { @@ -330,11 +391,13 @@ func (r *Reconciler) patchStatusError(ctx context.Context, addon *helmv1alpha1.H return reconcileErr } -func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) (reconcile.Result, error) { +func (r *Reconciler) updateStatusOnInternalRelease(ctx context.Context, addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) (reconcile.Result, error) { logger := log.FromContext(ctx) base := addon.DeepCopy() + addonReadyCond := meta.FindStatusCondition(addon.Status.Conditions, ConditionTypeReady) + internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) if internalReadyCond != nil { apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ @@ -358,20 +421,33 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC Reason: ReasonInstallSucceeded, Message: "", }) - addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ - HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, - HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, - Version: base.Spec.Chart.Version, + + // Required if chart or repository was changed and there was an existing chart. + apimeta.RemoveStatusCondition(&addon.Status.Conditions, ConditionTypeUpdateInstalled) + + if addonReadyCond != nil && addonReadyCond.Status == metav1.ConditionTrue { + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, + Version: base.Spec.Chart.Version, + } + addon.Status.LastAppliedValues = base.Spec.Values } - addon.Status.LastAppliedValues = base.Spec.Values case helmv2.UpgradeSucceededReason: - if r.isUpdateInstalled(internalHelmRelease) { + if r.isUpdateInstalled(addon, internalHelmRelease) { apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ Type: ConditionTypeUpdateInstalled, Status: metav1.ConditionTrue, Reason: ReasonUpdateSucceeded, Message: "", }) + if addonReadyCond != nil && addonReadyCond.Status == metav1.ConditionTrue { + addon.Status.LastAppliedChart = &helmv1alpha1.HelmClusterAddonLastAppliedChartRef{ + HelmClusterAddonChartName: base.Spec.Chart.HelmClusterAddonChartName, + HelmClusterAddonRepository: base.Spec.Chart.HelmClusterAddonRepository, + Version: base.Spec.Chart.Version, + } + } } else { if addonValues, err := helmchartutil.ReadValues(addon.Spec.Values.Raw); err != nil { logger.Error(err, "failed to decode values on LastAppliedValues update: %w", err) @@ -406,7 +482,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC Message: internalReadyCond.Message, }) case helmv2.UpgradeFailedReason: - if r.isUpdateInstalled(internalHelmRelease) { + if r.isUpdateInstalled(addon, internalHelmRelease) { apimeta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ Type: ConditionTypeUpdateInstalled, Status: metav1.ConditionFalse, @@ -450,7 +526,7 @@ func (r *Reconciler) updateStatus(ctx context.Context, addon *helmv1alpha1.HelmC } // isUpdateInstalled return true if new release was initiated due to chart name/version change, otherwise returns false. -func (r *Reconciler) isUpdateInstalled(internalHelmRelease *helmv2.HelmRelease) bool { +func (r *Reconciler) isUpdateInstalled(addon *helmv1alpha1.HelmClusterAddon, internalHelmRelease *helmv2.HelmRelease) bool { internalReadyCond := meta.FindStatusCondition(internalHelmRelease.Status.Conditions, ConditionTypeReady) if internalReadyCond == nil { return false @@ -460,7 +536,10 @@ func (r *Reconciler) isUpdateInstalled(internalHelmRelease *helmv2.HelmRelease) latest := internalHelmRelease.Status.History.Latest() previous := internalHelmRelease.Status.History.Previous(true) - if previous != nil && previous.Status == "superseded" && latest != nil && latest.VersionedChartName() != previous.VersionedChartName() { + if previous != nil && previous.Status == "superseded" && + latest != nil && + (latest.VersionedChartName() != previous.VersionedChartName() || + addon.Spec.Chart.HelmClusterAddonRepository != addon.Status.LastAppliedChart.HelmClusterAddonRepository) { return true } }