From a3de7197d349f8dcfa12859be8f7e160dc1813a6 Mon Sep 17 00:00:00 2001 From: Ilya Drey Date: Fri, 20 Feb 2026 10:07:59 +0300 Subject: [PATCH 1/2] 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 2/2] 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: